@vitronai/themis 0.1.13 → 0.1.15

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,21 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.1.15 - 2026-03-27
8
+
9
+ - Added a direct in-sidebar `Quick Actions` group plus an `Artifact Files` drawer to the in-repo VS Code extension scaffold so core Themis commands and raw artifact navigation remain reachable even when the VS Code view toolbar overflows.
10
+ - Improved `themis generate` onboarding and error guidance so downstream docs/templates now refer to `npx themis generate <source-root>`, and missing `src/` targets surface corrective suggestions for likely repo layouts such as `app/`, `pages/`, or repo-root scans.
11
+
12
+ ## 0.1.14 - 2026-03-27
13
+
7
14
  - 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
15
  - 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
16
  - 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
17
  - Added native React showcase fixtures for Themis, Jest, and Vitest plus a dedicated first-party Themis CI showcase job.
11
18
  - 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
19
  - Added ESLint with a dedicated CI lint job and folded lint into local validation and prepublish checks.
20
+ - 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.
21
+ - 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
22
 
14
23
  ## 0.1.4 - 2026-03-26
15
24
 
package/README.md CHANGED
@@ -18,10 +18,12 @@ If you are a human or AI agent adopting Themis in another repo, use:
18
18
  ```bash
19
19
  npm install -D @vitronai/themis@latest
20
20
  npx themis init --agents
21
- npx themis generate src
21
+ npx themis generate <source-root>
22
22
  npx themis test
23
23
  ```
24
24
 
25
+ Use `src` for conventional source trees and `app` for Next App Router repos.
26
+
25
27
  - `npx themis init --agents` writes `themis.config.json`, updates `.gitignore`, and scaffolds a downstream `AGENTS.md` when one does not already exist.
26
28
  - machine-readable agent manifest: [`themis.ai.json`](themis.ai.json)
27
29
  - downstream adoption guide: [`docs/agents-adoption.md`](docs/agents-adoption.md)
@@ -93,7 +95,7 @@ Themis is built for modern Node.js and TypeScript projects:
93
95
 
94
96
  ## Adopt In Another Repo
95
97
 
96
- Use the AI Quickstart above as the canonical install/generate/test flow. Generated files land under `__themis__/tests` by default.
98
+ Use the AI Quickstart above as the canonical install/generate/test flow. Replace `<source-root>` with the repo's actual source tree such as `src` or `app`. 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
99
  TypeScript-generated suites use `import` syntax so downstream ESLint and ESM-style rules do not flag Themis output as legacy `require(...)` code.
98
100
 
99
101
  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 +298,7 @@ Short version:
296
298
  - 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
299
  - Themis React Showcase job verifies a straight-up native Themis React fixture as a first-party example.
298
300
  - 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.
301
+ - Release `0.1.15` packages this expanded proof lane so every CI run now proves the provider-heavy example alongside the earlier fixtures.
300
302
 
301
303
  ## Agent Guide
302
304
 
@@ -0,0 +1 @@
1
+ export * from './src/contract-runtime';
@@ -7,7 +7,7 @@ Use this guide when you want another repository to adopt Themis and make that ch
7
7
  ```bash
8
8
  npm install -D @vitronai/themis@latest
9
9
  npx themis init --agents
10
- npx themis generate src
10
+ npx themis generate <source-root>
11
11
  npx themis test
12
12
  ```
13
13
 
@@ -16,7 +16,7 @@ What those commands do:
16
16
  - `npm install -D @vitronai/themis`: installs Themis as the repo's unit test framework
17
17
  - `npx themis init`: creates `themis.config.json` and adds `.themis/`, `__themis__/reports/`, and `__themis__/shims/` to `.gitignore`
18
18
  - `npx themis init --agents`: does the same and scaffolds a downstream `AGENTS.md` when one does not already exist
19
- - `npx themis generate src`: generates deterministic unit tests for JS/TS exports under `__themis__/tests`, using `.generated.test.ts` for TS/TSX sources and `.generated.test.js` for JS/JSX sources
19
+ - `npx themis generate <source-root>`: generates deterministic unit tests for JS/TS exports under `__themis__/tests`, using `.generated.test.ts` for TS/TSX sources and `.generated.test.js` for JS/JSX sources
20
20
  - `npx themis test`: runs the suite
21
21
 
22
22
  ## Migrate From Jest Or Vitest
@@ -51,7 +51,7 @@ Copy the contents of [`templates/AGENTS.themis.md`](../templates/AGENTS.themis.m
51
51
  Use `@vitronai/themis` as the project's unit test framework.
52
52
  Install with `npm install -D @vitronai/themis`.
53
53
  Initialize with `npx themis init`.
54
- Generate tests with `npx themis generate src`.
54
+ Generate tests with `npx themis generate <source-root>` such as `src` or `app`.
55
55
  Run tests with `npx themis test`.
56
56
  Keep `.themis/`, `__themis__/reports/`, and `__themis__/shims/` in `.gitignore`.
57
57
  Expect generated tests to land under `__themis__/tests` with `.generated.test.ts` for TS/TSX sources and `.generated.test.js` for JS/JSX sources.
package/docs/api.md CHANGED
@@ -11,11 +11,11 @@ Use it in a repo with:
11
11
  ```bash
12
12
  npm install -D @vitronai/themis
13
13
  npx themis init --agents
14
- npx themis generate src
14
+ npx themis generate <source-root>
15
15
  npx themis test
16
16
  ```
17
17
 
18
- `npx themis generate src` writes generated tests under `__themis__/tests` by default.
18
+ Use `src` for conventional source trees and `app` for Next App Router repos. `npx themis generate <source-root>` writes generated tests under `__themis__/tests` by default.
19
19
 
20
20
  For downstream repo setup and copyable agent instructions, see [`docs/agents-adoption.md`](agents-adoption.md) and [`templates/AGENTS.themis.md`](../templates/AGENTS.themis.md).
21
21
  For machine-readable agent adoption metadata, see [`themis.ai.json`](../themis.ai.json).
@@ -59,7 +59,7 @@ Themis uses generation and explicit assertions as a contract-first alternative t
59
59
 
60
60
  Default behavior:
61
61
 
62
- - input directory: `src`
62
+ - input directory when omitted: `src`
63
63
  - output directory: `__themis__/tests`
64
64
  - generated files mirror the scanned source tree with `*.generated.test.ts` for TS/TSX sources and `*.generated.test.js` for JS/JSX sources
65
65
  - generated TypeScript suites emit `import` syntax so downstream lint and ESM rules do not reject Themis output for using `require(...)`
@@ -77,6 +77,8 @@ Default behavior:
77
77
  - `.themis/generate/generate-handoff.json` stores a compact prompt-ready handoff payload for agents
78
78
  - `.themis/generate/generate-backlog.json` stores unresolved skips, conflicts, and confidence debt with suggested remediation
79
79
 
80
+ If `src/` does not exist but the repo uses `app/` or `pages/`, pass that path explicitly. Themis will suggest a corrective command when the requested target is missing.
81
+
80
82
  ## `themis generate` options
81
83
 
82
84
  | Option | Type | Description |
@@ -6,7 +6,9 @@ This is the intended shape of the editor UX:
6
6
 
7
7
  - a Themis activity-bar container
8
8
  - a results sidebar driven by `.themis/**` artifacts
9
+ - an in-view quick-actions group so the main commands remain reachable when the VS Code view toolbar hides overflow actions
9
10
  - commands to run tests, rerun failures, refresh results, and open the HTML report
11
+ - an artifact-files drawer for opening raw `.themis/**` payloads directly in the editor
10
12
  - commands to accept reviewed contract baselines and rerun migration codemods
11
13
  - failure navigation that jumps from artifact data into the source file
12
14
  - generated-review navigation for source files, generated tests, hint sidecars, and backlog items
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitronai/themis",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",
package/src/cli.js CHANGED
@@ -23,7 +23,7 @@ async function main(argv) {
23
23
  if (command === 'init') {
24
24
  const initFlags = parseInitFlags(argv.slice(1));
25
25
  const initResult = runInit(cwd, initFlags);
26
- console.log('Themis initialized. Next: npx themis generate src && npx themis test');
26
+ console.log('Themis initialized. Next: npx themis generate <source-root> && npx themis test');
27
27
  if (initFlags.agents) {
28
28
  if (initResult && initResult.path && initResult.created) {
29
29
  console.log(`Agents: created ${formatCliPath(cwd, initResult.path)} from the Themis downstream template.`);
@@ -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
@@ -609,7 +609,7 @@ function resolveScanTarget(projectRoot, targetDir) {
609
609
  const requestedPath = path.resolve(projectRoot, targetDir);
610
610
 
611
611
  if (!fs.existsSync(requestedPath)) {
612
- throw new Error(`Generate target not found: ${formatPathForDisplay(projectRoot, requestedPath)}`);
612
+ throw new Error(buildMissingGenerateTargetMessage(projectRoot, requestedPath));
613
613
  }
614
614
 
615
615
  const stat = fs.statSync(requestedPath);
@@ -632,6 +632,100 @@ function resolveScanTarget(projectRoot, targetDir) {
632
632
  };
633
633
  }
634
634
 
635
+ function buildMissingGenerateTargetMessage(projectRoot, requestedPath) {
636
+ const requestedDisplay = formatPathForDisplay(projectRoot, requestedPath);
637
+ const suggestions = collectGenerateTargetSuggestions(projectRoot, requestedPath);
638
+
639
+ if (suggestions.length === 0) {
640
+ return `Generate target not found: ${requestedDisplay}`;
641
+ }
642
+
643
+ return [
644
+ `Generate target not found: ${requestedDisplay}`,
645
+ 'Detected likely source roots in this repo:',
646
+ ...suggestions.map((suggestion) => `- ${suggestion.label}: npx themis generate ${suggestion.commandTarget}`)
647
+ ].join('\n');
648
+ }
649
+
650
+ function collectGenerateTargetSuggestions(projectRoot, requestedPath) {
651
+ const requestedName = path.basename(requestedPath);
652
+ const suggestions = [];
653
+ const candidateDirs = [
654
+ {
655
+ label: 'Next app router source',
656
+ dir: path.join(projectRoot, 'app'),
657
+ commandTarget: 'app'
658
+ },
659
+ {
660
+ label: 'Pages router source',
661
+ dir: path.join(projectRoot, 'pages'),
662
+ commandTarget: 'pages'
663
+ },
664
+ {
665
+ label: 'Source tree',
666
+ dir: path.join(projectRoot, 'src'),
667
+ commandTarget: 'src'
668
+ }
669
+ ];
670
+
671
+ for (const candidate of candidateDirs) {
672
+ if (candidate.dir === requestedPath || path.basename(candidate.dir) === requestedName) {
673
+ continue;
674
+ }
675
+ if (!containsEligibleSourceFiles(candidate.dir)) {
676
+ continue;
677
+ }
678
+ suggestions.push(candidate);
679
+ }
680
+
681
+ if (containsEligibleSourceFiles(projectRoot) && requestedPath !== projectRoot) {
682
+ suggestions.push({
683
+ label: 'Repo root scan',
684
+ dir: projectRoot,
685
+ commandTarget: '.'
686
+ });
687
+ }
688
+
689
+ return suggestions;
690
+ }
691
+
692
+ function containsEligibleSourceFiles(dirPath) {
693
+ if (!dirPath || !fs.existsSync(dirPath)) {
694
+ return false;
695
+ }
696
+
697
+ const stat = fs.statSync(dirPath);
698
+ if (stat.isFile()) {
699
+ return isEligibleSourceFile(dirPath);
700
+ }
701
+ if (!stat.isDirectory()) {
702
+ return false;
703
+ }
704
+
705
+ const stack = [dirPath];
706
+ while (stack.length > 0) {
707
+ const current = stack.pop();
708
+ const entries = fs.readdirSync(current, { withFileTypes: true });
709
+ for (const entry of entries) {
710
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.themis' || entry.name === '__themis__') {
711
+ continue;
712
+ }
713
+
714
+ const fullPath = path.join(current, entry.name);
715
+ if (entry.isDirectory()) {
716
+ stack.push(fullPath);
717
+ continue;
718
+ }
719
+
720
+ if (entry.isFile() && isEligibleSourceFile(fullPath)) {
721
+ return true;
722
+ }
723
+ }
724
+ }
725
+
726
+ return false;
727
+ }
728
+
635
729
  function inferMirrorBase(projectRoot, filePath) {
636
730
  const relative = normalizePath(path.relative(projectRoot, filePath));
637
731
  const segments = relative.split('/');
@@ -3233,6 +3327,8 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
3233
3327
  const sourceImport = normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.file));
3234
3328
  const sourceAbsolutePath = normalizePath(path.relative(path.dirname(outputFile), analysis.file));
3235
3329
  const useImportSyntax = path.extname(outputFile).toLowerCase() === '.ts';
3330
+ const typeReferences = useImportSyntax ? '/// <reference types="@vitronai/themis/globals" />\n' : '';
3331
+ const typePrelude = useImportSyntax ? `${renderGeneratedTypePrelude()}\n\n` : '';
3236
3332
  const expectedExportContracts = buildExpectedExportContracts(analysis);
3237
3333
  const providerImport = analysis.projectProviderFile
3238
3334
  ? normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.projectProviderFile))
@@ -3246,48 +3342,182 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
3246
3342
  : renderGeneratedRequirePrelude({ helperImport });
3247
3343
  const providerLoadExpression = useImportSyntax ? 'PROJECT_PROVIDER_MODULE' : 'require(PROJECT_PROVIDER_IMPORT)';
3248
3344
  const loadModuleBody = useImportSyntax
3249
- ? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT);'
3345
+ ? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT) as Promise<RuntimeModuleExports>;'
3250
3346
  : ' 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);';
3347
+ const sourceFileDeclaration = useImportSyntax
3348
+ ? 'const SOURCE_FILE = require.resolve(SOURCE_IMPORT);'
3349
+ : `const SOURCE_FILE = path.resolve(__dirname, ${JSON.stringify(sourceAbsolutePath)});`;
3350
+ const scannedExportsDeclaration = useImportSyntax
3351
+ ? `const SCANNED_EXPORTS: readonly string[] | null = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};`
3352
+ : `const SCANNED_EXPORTS = ${analysis.exactExports ? JSON.stringify(analysis.exportNames, null, 2) : 'null'};`;
3353
+ const expectedExportDeclaration = useImportSyntax
3354
+ ? `const EXPECTED_EXPORT_CONTRACTS: RuntimeModuleContractExpectations = ${JSON.stringify(expectedExportContracts, null, 2)};`
3355
+ : `const EXPECTED_EXPORT_CONTRACTS = ${JSON.stringify(expectedExportContracts, null, 2)};`;
3356
+ const providerImportDeclaration = useImportSyntax
3357
+ ? `const PROJECT_PROVIDER_IMPORT: string | null = ${providerImport ? JSON.stringify(providerImport) : 'null'};`
3358
+ : `const PROJECT_PROVIDER_IMPORT = ${providerImport ? JSON.stringify(providerImport) : 'null'};`;
3359
+ const providerIndexesDeclaration = useImportSyntax
3360
+ ? `const PROJECT_PROVIDER_INDEXES: readonly number[] = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};`
3361
+ : `const PROJECT_PROVIDER_INDEXES = ${JSON.stringify(analysis.providerRuntimeIndexes || [], null, 2)};`;
3362
+ const providerPresetsDeclaration = useImportSyntax
3363
+ ? `const PROJECT_PROVIDER_PRESETS: readonly ProviderPreset[] = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};`
3364
+ : `const PROJECT_PROVIDER_PRESETS = ${JSON.stringify(analysis.providerRuntimePresets || [], null, 2)};`;
3365
+ const nextAppCasesDeclaration = useImportSyntax
3366
+ ? `const NEXT_APP_CASES: readonly ComponentCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};`
3367
+ : `const NEXT_APP_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-app-component'), null, 2)};`;
3368
+ const nextRouteCasesDeclaration = useImportSyntax
3369
+ ? `const NEXT_ROUTE_CASES: readonly RouteCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};`
3370
+ : `const NEXT_ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'next-route-handler'), null, 2)};`;
3371
+ const componentCasesDeclaration = useImportSyntax
3372
+ ? `const COMPONENT_CASES: readonly ComponentCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};`
3373
+ : `const COMPONENT_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-component'), null, 2)};`;
3374
+ const hookCasesDeclaration = useImportSyntax
3375
+ ? `const HOOK_CASES: readonly HookCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};`
3376
+ : `const HOOK_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'react-hook'), null, 2)};`;
3377
+ const routeCasesDeclaration = useImportSyntax
3378
+ ? `const ROUTE_CASES: readonly RouteCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};`
3379
+ : `const ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'route-handler'), null, 2)};`;
3380
+ const serviceCasesDeclaration = useImportSyntax
3381
+ ? `const SERVICE_CASES: readonly ServiceCase[] = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};`
3382
+ : `const SERVICE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};`;
3383
+ const loadModuleSignature = useImportSyntax
3384
+ ? 'function loadModuleExports(): Promise<RuntimeModuleExports> {'
3385
+ : 'function loadModuleExports() {';
3386
+ const resolveProvidersSignature = useImportSyntax
3387
+ ? 'function resolveProjectProviders(loaded: unknown): ProjectProviderRuntime[] {'
3388
+ : 'function resolveProjectProviders(loaded) {';
3389
+ const applyMocksSignature = useImportSyntax
3390
+ ? 'function applyProjectProviderMocks(exportName: string, scenarioName: ScenarioName): void {'
3391
+ : 'function applyProjectProviderMocks(exportName, scenarioName) {';
3392
+ const applyRenderSignature = useImportSyntax
3393
+ ? 'function applyProjectProviderRender(element: unknown, exportName: string, scenarioName: ScenarioName): unknown {'
3394
+ : 'function applyProjectProviderRender(element, exportName, scenarioName) {';
3395
+ const createRenderOptionsSignature = useImportSyntax
3396
+ ? 'function createProjectProviderRenderOptions(exportName: string, scenarioName: ScenarioName): { wrapRender(element: unknown): unknown } {'
3397
+ : 'function createProjectProviderRenderOptions(exportName, scenarioName) {';
3398
+ const applyPresetsSignature = useImportSyntax
3399
+ ? 'function applyProjectProviderPresets(element: unknown): unknown {'
3400
+ : 'function applyProjectProviderPresets(element) {';
3401
+ const withProviderShellSignature = useImportSyntax
3402
+ ? 'function withProviderShell(type: string, element: unknown, attributes: ProviderConfig = {}): ReactTestElement {'
3403
+ : 'function withProviderShell(type, element, attributes = {}) {';
3404
+ const providerHelperSignature = (name) => useImportSyntax
3405
+ ? `function ${name}(element: unknown, config: ProviderConfig = {}): ReactTestElement {`
3406
+ : `function ${name}(element, config = {}) {`;
3407
+ const serializeProviderDataSignature = useImportSyntax
3408
+ ? 'function serializeProviderData(value: unknown): string {'
3409
+ : 'function serializeProviderData(value) {';
3410
+ const assertExpectedRuntimeContractSignature = useImportSyntax
3411
+ ? 'function assertExpectedRuntimeContract(runtime: RuntimeModuleExports): void {'
3412
+ : 'function assertExpectedRuntimeContract(runtime) {';
3413
+ const assertNormalizedRenderContractSignature = useImportSyntax
3414
+ ? 'function assertNormalizedRenderContract(rendered: unknown): void {'
3415
+ : 'function assertNormalizedRenderContract(rendered) {';
3416
+ const assertDomContractShapeSignature = useImportSyntax
3417
+ ? 'function assertDomContractShape(contract: DomContract): void {'
3418
+ : 'function assertDomContractShape(contract) {';
3419
+ const countPlannedStepsSignature = useImportSyntax
3420
+ ? 'function countPlannedSteps(plan: readonly RepeatedPlanStep[] | null | undefined): number {'
3421
+ : 'function countPlannedSteps(plan) {';
3422
+ const assertComponentInteractionContractShapeSignature = useImportSyntax
3423
+ ? 'function assertComponentInteractionContractShape(contract: ComponentInteractionContract, plan: readonly InteractionPlanStep[] | null | undefined): void {'
3424
+ : 'function assertComponentInteractionContractShape(contract, plan) {';
3425
+ const assertFlowContractShapeSignature = useImportSyntax
3426
+ ? 'function assertFlowContractShape(flow: ComponentBehaviorFlowContract, plan: readonly FlowPlanStep[] | null | undefined): void {'
3427
+ : 'function assertFlowContractShape(flow, plan) {';
3428
+ const assertHookResultContractSignature = useImportSyntax
3429
+ ? 'function assertHookResultContract(result: unknown): void {'
3430
+ : 'function assertHookResultContract(result) {';
3431
+ const assertHookInteractionContractShapeSignature = useImportSyntax
3432
+ ? 'function assertHookInteractionContractShape(contract: HookInteractionContract, plan: readonly HookInteractionPlanStep[] | null | undefined): void {'
3433
+ : 'function assertHookInteractionContractShape(contract, plan) {';
3434
+ const assertRouteResultContractShapeSignature = useImportSyntax
3435
+ ? 'function assertRouteResultContractShape(response: unknown): void {'
3436
+ : 'function assertRouteResultContractShape(response) {';
3437
+ const assertServiceResultContractShapeSignature = useImportSyntax
3438
+ ? 'function assertServiceResultContractShape(result: unknown): void {'
3439
+ : 'function assertServiceResultContractShape(result) {';
3440
+ const componentReadExpression = useImportSyntax
3441
+ ? 'readExportValue<(props: Record<string, unknown>) => unknown>(moduleExports, testCase.exportName)'
3442
+ : 'readExportValue(moduleExports, testCase.exportName)';
3443
+ const hookReadExpression = useImportSyntax
3444
+ ? 'readExportValue<(...args: unknown[]) => unknown>(moduleExports, testCase.exportName)'
3445
+ : 'readExportValue(moduleExports, testCase.exportName)';
3446
+ const handlerReadExpression = useImportSyntax
3447
+ ? 'readExportValue<(request: unknown, context?: unknown) => unknown>(moduleExports, testCase.exportName)'
3448
+ : 'readExportValue(moduleExports, testCase.exportName)';
3449
+ const serviceReadExpression = useImportSyntax
3450
+ ? 'readExportValue<(...args: unknown[]) => unknown>(moduleExports, testCase.exportName)'
3451
+ : 'readExportValue(moduleExports, testCase.exportName)';
3452
+ const interactionContractExpression = useImportSyntax
3453
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentInteractionContract'
3454
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3455
+ const nextDomContractExpression = useImportSyntax
3456
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentInteractionContract'
3457
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3458
+ const nextFlowExpression = useImportSyntax
3459
+ ? 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\')) as ComponentBehaviorFlowContract'
3460
+ : 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'next-app-component\'))';
3461
+ const componentInteractionExpression = useImportSyntax
3462
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentInteractionContract'
3463
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3464
+ const componentDomExpression = useImportSyntax
3465
+ ? 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentInteractionContract'
3466
+ : 'await runComponentInteractionContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.interactions, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3467
+ const componentFlowExpression = useImportSyntax
3468
+ ? 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'react-component\')) as ComponentBehaviorFlowContract'
3469
+ : 'await runComponentBehaviorFlowContract(SOURCE_FILE, testCase.exportName, testCase.props, testCase.flows, createProjectProviderRenderOptions(testCase.exportName, \'react-component\'))';
3470
+ const hookInteractionExpression = useImportSyntax
3471
+ ? 'runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions) as HookInteractionContract'
3472
+ : 'runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions)';
3251
3473
 
3252
3474
  return `${GENERATED_MARKER}
3253
- // Source: ${relativeSourcePath}
3475
+ ${typeReferences}// Source: ${relativeSourcePath}
3254
3476
 
3255
3477
  ${runtimePrelude}
3256
-
3257
- const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
3478
+ ${typePrelude}const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
3258
3479
  const SOURCE_IMPORT = ${JSON.stringify(sourceImport)};
3259
- const SOURCE_FILE = path.resolve(__dirname, ${JSON.stringify(sourceAbsolutePath)});
3480
+ ${sourceFileDeclaration}
3260
3481
  const SOURCE_HASH = ${JSON.stringify(analysis.sourceHash)};
3261
3482
  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() {
3483
+ ${scannedExportsDeclaration}
3484
+ ${expectedExportDeclaration}
3485
+ ${providerImportDeclaration}
3486
+ ${providerIndexesDeclaration}
3487
+ ${providerPresetsDeclaration}
3488
+ ${nextAppCasesDeclaration}
3489
+ ${nextRouteCasesDeclaration}
3490
+ ${componentCasesDeclaration}
3491
+ ${hookCasesDeclaration}
3492
+ ${routeCasesDeclaration}
3493
+ ${serviceCasesDeclaration}
3494
+
3495
+ ${loadModuleSignature}
3275
3496
  ${loadModuleBody}
3276
3497
  }
3277
3498
 
3278
- function applyProjectProviderMocks(exportName, scenarioName) {
3499
+ ${resolveProvidersSignature}
3500
+ if (Array.isArray(loaded)) {
3501
+ return loaded;
3502
+ }
3503
+
3504
+ if (loaded && typeof loaded === 'object') {
3505
+ const moduleValue = ${useImportSyntax ? 'loaded as { providers?: ProjectProviderRuntime[] } & ProjectProviderRuntime' : 'loaded'};
3506
+ if (Array.isArray(moduleValue.providers)) {
3507
+ return moduleValue.providers;
3508
+ }
3509
+ return [moduleValue];
3510
+ }
3511
+
3512
+ return [];
3513
+ }
3514
+
3515
+ ${applyMocksSignature}
3279
3516
  if (!PROJECT_PROVIDER_IMPORT || PROJECT_PROVIDER_INDEXES.length === 0) {
3280
3517
  return;
3281
3518
  }
3282
3519
 
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
- : [];
3520
+ const providers = resolveProjectProviders(${providerLoadExpression});
3291
3521
 
3292
3522
  for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
3293
3523
  const provider = providers[providerIndex];
@@ -3314,21 +3544,14 @@ function applyProjectProviderMocks(exportName, scenarioName) {
3314
3544
  }
3315
3545
  }
3316
3546
 
3317
- function applyProjectProviderRender(element, exportName, scenarioName) {
3547
+ ${applyRenderSignature}
3318
3548
  let current = applyProjectProviderPresets(element);
3319
3549
 
3320
3550
  if (!PROJECT_PROVIDER_IMPORT) {
3321
3551
  return current;
3322
3552
  }
3323
3553
 
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
- : [];
3554
+ const providers = resolveProjectProviders(${providerLoadExpression});
3332
3555
 
3333
3556
  for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
3334
3557
  const provider = providers[providerIndex];
@@ -3343,24 +3566,12 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
3343
3566
  scenario: scenarioName,
3344
3567
  element: current,
3345
3568
  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
- }
3569
+ withReactRouter,
3570
+ withNextNavigation,
3571
+ withAuthSession,
3572
+ withReactQuery,
3573
+ withZustandStore,
3574
+ withReduxStore
3364
3575
  });
3365
3576
 
3366
3577
  if (nextValue !== undefined) {
@@ -3371,7 +3582,13 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
3371
3582
  return current;
3372
3583
  }
3373
3584
 
3374
- function applyProjectProviderPresets(element) {
3585
+ ${createRenderOptionsSignature}
3586
+ return {
3587
+ wrapRender: (element${useImportSyntax ? ': unknown' : ''}) => applyProjectProviderRender(element, exportName, scenarioName)
3588
+ };
3589
+ }
3590
+
3591
+ ${applyPresetsSignature}
3375
3592
  let current = element;
3376
3593
 
3377
3594
  for (const preset of PROJECT_PROVIDER_PRESETS) {
@@ -3398,7 +3615,7 @@ function applyProjectProviderPresets(element) {
3398
3615
  return current;
3399
3616
  }
3400
3617
 
3401
- function withProviderShell(type, element, attributes = {}) {
3618
+ ${withProviderShellSignature}
3402
3619
  return {
3403
3620
  $$typeof: 'react.test.element',
3404
3621
  type,
@@ -3411,7 +3628,7 @@ function withProviderShell(type, element, attributes = {}) {
3411
3628
  };
3412
3629
  }
3413
3630
 
3414
- function withReactRouter(element, config = {}) {
3631
+ ${providerHelperSignature('withReactRouter')}
3415
3632
  return withProviderShell('themis-router-provider', element, {
3416
3633
  role: 'navigation',
3417
3634
  'data-themis-provider': 'router',
@@ -3424,7 +3641,7 @@ function withReactRouter(element, config = {}) {
3424
3641
  });
3425
3642
  }
3426
3643
 
3427
- function withReactQuery(element, config = {}) {
3644
+ ${providerHelperSignature('withReactQuery')}
3428
3645
  return withProviderShell('themis-react-query-provider', element, {
3429
3646
  'data-themis-provider': 'react-query',
3430
3647
  'data-query-client': typeof config.clientName === 'string' ? config.clientName : 'themis-query-client',
@@ -3436,7 +3653,7 @@ function withReactQuery(element, config = {}) {
3436
3653
  });
3437
3654
  }
3438
3655
 
3439
- function withNextNavigation(element, config = {}) {
3656
+ ${providerHelperSignature('withNextNavigation')}
3440
3657
  return withProviderShell('themis-next-navigation-provider', element, {
3441
3658
  'data-themis-provider': 'next-navigation',
3442
3659
  'data-next-pathname': typeof config.pathname === 'string' ? config.pathname : '/',
@@ -3447,7 +3664,7 @@ function withNextNavigation(element, config = {}) {
3447
3664
  });
3448
3665
  }
3449
3666
 
3450
- function withAuthSession(element, config = {}) {
3667
+ ${providerHelperSignature('withAuthSession')}
3451
3668
  return withProviderShell('themis-auth-provider', element, {
3452
3669
  'data-themis-provider': 'auth',
3453
3670
  'data-auth-user': typeof config.user === 'string' ? config.user : 'anonymous',
@@ -3458,7 +3675,7 @@ function withAuthSession(element, config = {}) {
3458
3675
  });
3459
3676
  }
3460
3677
 
3461
- function withZustandStore(element, config = {}) {
3678
+ ${providerHelperSignature('withZustandStore')}
3462
3679
  return withProviderShell('themis-zustand-provider', element, {
3463
3680
  'data-themis-provider': 'zustand',
3464
3681
  'data-store-name': typeof config.name === 'string' ? config.name : 'zustand-store',
@@ -3468,7 +3685,7 @@ function withZustandStore(element, config = {}) {
3468
3685
  });
3469
3686
  }
3470
3687
 
3471
- function withReduxStore(element, config = {}) {
3688
+ ${providerHelperSignature('withReduxStore')}
3472
3689
  return withProviderShell('themis-redux-provider', element, {
3473
3690
  'data-themis-provider': 'redux',
3474
3691
  'data-redux-slice': typeof config.slice === 'string' ? config.slice : 'root',
@@ -3478,14 +3695,14 @@ function withReduxStore(element, config = {}) {
3478
3695
  });
3479
3696
  }
3480
3697
 
3481
- function serializeProviderData(value) {
3698
+ ${serializeProviderDataSignature}
3482
3699
  if (value === undefined) {
3483
3700
  return '';
3484
3701
  }
3485
3702
  return JSON.stringify(normalizeBehaviorValue(value));
3486
3703
  }
3487
3704
 
3488
- function assertExpectedRuntimeContract(runtime) {
3705
+ ${assertExpectedRuntimeContractSignature}
3489
3706
  expect(typeof runtime).toBe('object');
3490
3707
  expect(runtime === null).toBe(false);
3491
3708
 
@@ -3495,7 +3712,7 @@ function assertExpectedRuntimeContract(runtime) {
3495
3712
  }
3496
3713
 
3497
3714
  for (const [exportName, expected] of Object.entries(EXPECTED_EXPORT_CONTRACTS)) {
3498
- const actual = runtime[exportName];
3715
+ const actual = ${useImportSyntax ? 'runtime[exportName] as Record<string, unknown>' : 'runtime[exportName]'};
3499
3716
  expect(Boolean(actual)).toBe(true);
3500
3717
 
3501
3718
  if (expected.kind && expected.kind !== 'unknown') {
@@ -3512,7 +3729,7 @@ function assertExpectedRuntimeContract(runtime) {
3512
3729
  }
3513
3730
  }
3514
3731
 
3515
- function assertNormalizedRenderContract(rendered) {
3732
+ ${assertNormalizedRenderContractSignature}
3516
3733
  const normalized = normalizeBehaviorValue(rendered);
3517
3734
  expect(normalized !== undefined && normalized !== null).toBe(true);
3518
3735
 
@@ -3521,21 +3738,22 @@ function assertNormalizedRenderContract(rendered) {
3521
3738
  return;
3522
3739
  }
3523
3740
 
3524
- if (typeof normalized === 'object') {
3525
- if (normalized.kind === 'element') {
3526
- expect(Boolean(normalized.type)).toBe(true);
3527
- expect(typeof normalized.props).toBe('object');
3741
+ if (normalized && typeof normalized === 'object') {
3742
+ const normalizedRecord = ${useImportSyntax ? 'normalized as Record<string, unknown>' : 'normalized'};
3743
+ if (normalizedRecord.kind === 'element') {
3744
+ expect(Boolean(normalizedRecord.type)).toBe(true);
3745
+ expect(typeof normalizedRecord.props).toBe('object');
3528
3746
  return;
3529
3747
  }
3530
3748
 
3531
- expect(Object.keys(normalized).length >= 0).toBe(true);
3749
+ expect(Object.keys(normalizedRecord).length >= 0).toBe(true);
3532
3750
  return;
3533
3751
  }
3534
3752
 
3535
3753
  expect(['string', 'number', 'boolean'].includes(typeof normalized)).toBe(true);
3536
3754
  }
3537
3755
 
3538
- function assertDomContractShape(contract) {
3756
+ ${assertDomContractShapeSignature}
3539
3757
  expect(typeof contract).toBe('object');
3540
3758
  expect(contract === null).toBe(false);
3541
3759
  expect(Array.isArray(contract.nodes)).toBe(true);
@@ -3543,7 +3761,7 @@ function assertDomContractShape(contract) {
3543
3761
  expect(typeof contract.textContent).toBe('string');
3544
3762
  }
3545
3763
 
3546
- function countPlannedSteps(plan) {
3764
+ ${countPlannedStepsSignature}
3547
3765
  if (!Array.isArray(plan)) {
3548
3766
  return 0;
3549
3767
  }
@@ -3554,7 +3772,7 @@ function countPlannedSteps(plan) {
3554
3772
  }, 0);
3555
3773
  }
3556
3774
 
3557
- function assertComponentInteractionContractShape(contract, plan) {
3775
+ ${assertComponentInteractionContractShapeSignature}
3558
3776
  assertNormalizedRenderContract(contract.rendered);
3559
3777
  assertDomContractShape(contract.dom);
3560
3778
  expect(Array.isArray(contract.interactions)).toBe(true);
@@ -3571,7 +3789,7 @@ function assertComponentInteractionContractShape(contract, plan) {
3571
3789
  }
3572
3790
  }
3573
3791
 
3574
- function assertFlowContractShape(flow, plan) {
3792
+ ${assertFlowContractShapeSignature}
3575
3793
  assertDomContractShape(flow.dom);
3576
3794
  expect(Array.isArray(flow.steps)).toBe(true);
3577
3795
  expect(flow.steps.length).toBe(Array.isArray(plan) ? plan.length : 0);
@@ -3624,7 +3842,7 @@ function assertFlowContractShape(flow, plan) {
3624
3842
  }
3625
3843
 
3626
3844
  if (expected.expected && expected.expected.rolesInclude !== undefined) {
3627
- const expectedRoles = Array.isArray(expected.expected.rolesInclude)
3845
+ const expectedRoles${useImportSyntax ? ': string[]' : ''} = Array.isArray(expected.expected.rolesInclude)
3628
3846
  ? expected.expected.rolesInclude
3629
3847
  : [expected.expected.rolesInclude];
3630
3848
  const matchesRoles = [step.immediateDom, step.settledDom].some((dom) => {
@@ -3636,12 +3854,12 @@ function assertFlowContractShape(flow, plan) {
3636
3854
  }
3637
3855
  }
3638
3856
 
3639
- function assertHookResultContract(result) {
3857
+ ${assertHookResultContractSignature}
3640
3858
  const normalized = normalizeBehaviorValue(result);
3641
3859
  expect(normalized !== undefined).toBe(true);
3642
3860
  }
3643
3861
 
3644
- function assertHookInteractionContractShape(contract, plan) {
3862
+ ${assertHookInteractionContractShapeSignature}
3645
3863
  expect(Array.isArray(contract.interactions)).toBe(true);
3646
3864
 
3647
3865
  if (countPlannedSteps(plan) > 0) {
@@ -3654,18 +3872,19 @@ function assertHookInteractionContractShape(contract, plan) {
3654
3872
  }
3655
3873
  }
3656
3874
 
3657
- function assertRouteResultContractShape(response) {
3875
+ ${assertRouteResultContractShapeSignature}
3658
3876
  expect(response !== undefined && response !== null).toBe(true);
3659
3877
 
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');
3878
+ if (response && typeof response === 'object' && ${useImportSyntax ? '(response as Record<string, unknown>).kind' : 'response.kind'} === 'response') {
3879
+ const routeResponse = ${useImportSyntax ? 'response as { status: number; headers: unknown }' : 'response'};
3880
+ expect(typeof routeResponse.status).toBe('number');
3881
+ expect(routeResponse.status >= 100).toBe(true);
3882
+ expect(routeResponse.status < 600).toBe(true);
3883
+ expect(typeof routeResponse.headers).toBe('object');
3665
3884
  }
3666
3885
  }
3667
3886
 
3668
- function assertServiceResultContractShape(result) {
3887
+ ${assertServiceResultContractShapeSignature}
3669
3888
  expect(result !== undefined).toBe(true);
3670
3889
  }
3671
3890
 
@@ -3684,28 +3903,20 @@ describe(${JSON.stringify(suiteName)}, () => {
3684
3903
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3685
3904
  applyProjectProviderMocks(testCase.exportName, 'next-app-component');
3686
3905
  const moduleExports = await loadModuleExports();
3687
- const component = readExportValue(moduleExports, testCase.exportName);
3906
+ const component = ${componentReadExpression};
3688
3907
  const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'next-app-component');
3689
3908
  assertNormalizedRenderContract(rendered);
3690
3909
  });
3691
3910
 
3692
3911
  test(testCase.exportName + ' next interaction contract', async () => {
3693
3912
  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
- });
3913
+ const interaction = ${interactionContractExpression};
3699
3914
  assertComponentInteractionContractShape(interaction, testCase.interactions);
3700
3915
  });
3701
3916
 
3702
3917
  test(testCase.exportName + ' next dom state contract', async () => {
3703
3918
  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
- });
3919
+ const contract = ${nextDomContractExpression};
3709
3920
  assertDomContractShape(contract.dom);
3710
3921
  if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
3711
3922
  expect(contract.interactions.length > 0).toBe(true);
@@ -3715,11 +3926,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3715
3926
  if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
3716
3927
  test(testCase.exportName + ' next behavioral flow contract', async () => {
3717
3928
  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
- });
3929
+ const flow = ${nextFlowExpression};
3723
3930
  assertFlowContractShape(flow, testCase.flows);
3724
3931
  expect(flow.steps.some((step) => !step.skipped)).toBe(true);
3725
3932
  });
@@ -3734,28 +3941,20 @@ describe(${JSON.stringify(suiteName)}, () => {
3734
3941
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3735
3942
  applyProjectProviderMocks(testCase.exportName, 'react-component');
3736
3943
  const moduleExports = await loadModuleExports();
3737
- const component = readExportValue(moduleExports, testCase.exportName);
3944
+ const component = ${componentReadExpression};
3738
3945
  const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'react-component');
3739
3946
  assertNormalizedRenderContract(rendered);
3740
3947
  });
3741
3948
 
3742
3949
  test(testCase.exportName + ' interaction contract', async () => {
3743
3950
  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
- });
3951
+ const interaction = ${componentInteractionExpression};
3749
3952
  assertComponentInteractionContractShape(interaction, testCase.interactions);
3750
3953
  });
3751
3954
 
3752
3955
  test(testCase.exportName + ' dom state contract', async () => {
3753
3956
  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
- });
3957
+ const contract = ${componentDomExpression};
3759
3958
  assertDomContractShape(contract.dom);
3760
3959
  if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
3761
3960
  expect(contract.interactions.length > 0).toBe(true);
@@ -3765,11 +3964,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3765
3964
  if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
3766
3965
  test(testCase.exportName + ' behavioral flow contract', async () => {
3767
3966
  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
- });
3967
+ const flow = ${componentFlowExpression};
3773
3968
  assertFlowContractShape(flow, testCase.flows);
3774
3969
  expect(flow.steps.some((step) => !step.skipped)).toBe(true);
3775
3970
  });
@@ -3784,14 +3979,14 @@ describe(${JSON.stringify(suiteName)}, () => {
3784
3979
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3785
3980
  applyProjectProviderMocks(testCase.exportName, 'react-hook');
3786
3981
  const moduleExports = await loadModuleExports();
3787
- const hook = readExportValue(moduleExports, testCase.exportName);
3982
+ const hook = ${hookReadExpression};
3788
3983
  const result = hook(...testCase.args);
3789
3984
  assertHookResultContract(result);
3790
3985
  });
3791
3986
 
3792
3987
  test(testCase.exportName + ' interaction contract', () => {
3793
3988
  applyProjectProviderMocks(testCase.exportName, 'react-hook');
3794
- const interaction = runHookInteractionContract(SOURCE_FILE, testCase.exportName, testCase.args, testCase.interactions);
3989
+ const interaction = ${hookInteractionExpression};
3795
3990
  assertHookInteractionContractShape(interaction, testCase.interactions);
3796
3991
  });
3797
3992
  }
@@ -3804,7 +3999,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3804
3999
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3805
4000
  applyProjectProviderMocks(testCase.exportName, 'next-route-handler');
3806
4001
  const moduleExports = await loadModuleExports();
3807
- const handler = readExportValue(moduleExports, testCase.exportName);
4002
+ const handler = ${handlerReadExpression};
3808
4003
  const request = createRequestFromSpec(testCase.request);
3809
4004
  const response = await Promise.resolve(handler(request, testCase.context));
3810
4005
  const normalizedResponse = await normalizeRouteResult(response);
@@ -3820,7 +4015,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3820
4015
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3821
4016
  applyProjectProviderMocks(testCase.exportName, 'route-handler');
3822
4017
  const moduleExports = await loadModuleExports();
3823
- const handler = readExportValue(moduleExports, testCase.exportName);
4018
+ const handler = ${handlerReadExpression};
3824
4019
  const request = createRequestFromSpec(testCase.request);
3825
4020
  const response = await Promise.resolve(handler(request, testCase.context));
3826
4021
  const normalizedResponse = await normalizeRouteResult(response);
@@ -3836,7 +4031,7 @@ describe(${JSON.stringify(suiteName)}, () => {
3836
4031
  test(testCase.exportName + ' ' + testCase.caseName, async () => {
3837
4032
  applyProjectProviderMocks(testCase.exportName, 'node-service');
3838
4033
  const moduleExports = await loadModuleExports();
3839
- const service = readExportValue(moduleExports, testCase.exportName);
4034
+ const service = ${serviceReadExpression};
3840
4035
  const result = await Promise.resolve(service(...testCase.args));
3841
4036
  assertServiceResultContractShape(normalizeBehaviorValue(result));
3842
4037
  });
@@ -3866,10 +4061,9 @@ const {
3866
4061
  function renderGeneratedImportPrelude({ helperImport, providerImport }) {
3867
4062
  const providerLine = providerImport
3868
4063
  ? `import * as PROJECT_PROVIDER_MODULE from ${JSON.stringify(providerImport)};`
3869
- : 'const PROJECT_PROVIDER_MODULE = null;';
4064
+ : 'const PROJECT_PROVIDER_MODULE: unknown = null;';
3870
4065
 
3871
- return `import path from 'path';
3872
- import {
4066
+ return `import {
3873
4067
  listExportNames,
3874
4068
  buildModuleContract,
3875
4069
  readExportValue,
@@ -3881,9 +4075,228 @@ import {
3881
4075
  runComponentBehaviorFlowContract,
3882
4076
  runHookInteractionContract
3883
4077
  } from ${JSON.stringify(helperImport)};
4078
+ import type {
4079
+ AdvanceTimersByTime,
4080
+ FlushMicrotasks,
4081
+ Fn,
4082
+ MockFetch,
4083
+ MockModule,
4084
+ ResetFetchMocks,
4085
+ RestoreFetch,
4086
+ RunAllTimers,
4087
+ UseFakeTimers,
4088
+ UseRealTimers
4089
+ } from "@vitronai/themis";
3884
4090
  ${providerLine}`;
3885
4091
  }
3886
4092
 
4093
+ function renderGeneratedTypePrelude() {
4094
+ return `declare const require: {
4095
+ resolve(id: string): string;
4096
+ };
4097
+
4098
+ type ScenarioName =
4099
+ | 'next-app-component'
4100
+ | 'next-route-handler'
4101
+ | 'react-component'
4102
+ | 'react-hook'
4103
+ | 'route-handler'
4104
+ | 'node-service';
4105
+ type RuntimeModuleExports = Record<string, unknown>;
4106
+ type RuntimeModuleContractExpectations = Record<string, {
4107
+ kind: string;
4108
+ arity: number | null;
4109
+ prototypeKeys: string[] | null;
4110
+ }>;
4111
+ type ProviderConfig = Record<string, unknown>;
4112
+
4113
+ interface RepeatedPlanStep {
4114
+ repeat?: number | null;
4115
+ }
4116
+
4117
+ interface InteractionPlanStep extends RepeatedPlanStep {
4118
+ event?: string | null;
4119
+ labelIncludes?: string | null;
4120
+ elementType?: string | null;
4121
+ }
4122
+
4123
+ interface FlowExpectation {
4124
+ immediateTextIncludes?: string;
4125
+ beforeTextIncludes?: string;
4126
+ settledTextIncludes?: string;
4127
+ textExcludes?: string;
4128
+ attributes?: Record<string, unknown>;
4129
+ rolesInclude?: string | string[];
4130
+ }
4131
+
4132
+ interface FlowPlanStep extends RepeatedPlanStep {
4133
+ label?: string | null;
4134
+ event: string;
4135
+ labelIncludes?: string | null;
4136
+ elementType?: string | null;
4137
+ target?: Record<string, unknown>;
4138
+ awaitResult?: boolean;
4139
+ flushMicrotasks?: number;
4140
+ advanceTimersByTime?: number;
4141
+ runAllTimers?: boolean;
4142
+ expected?: FlowExpectation;
4143
+ }
4144
+
4145
+ interface HookInteractionPlanStep extends RepeatedPlanStep {
4146
+ method?: string | null;
4147
+ }
4148
+
4149
+ interface ComponentCase {
4150
+ exportName: string;
4151
+ displayName: string;
4152
+ caseName: string;
4153
+ props: Record<string, unknown>;
4154
+ interactions: InteractionPlanStep[];
4155
+ flows: FlowPlanStep[];
4156
+ confidence: string;
4157
+ }
4158
+
4159
+ interface HookCase {
4160
+ exportName: string;
4161
+ displayName: string;
4162
+ caseName: string;
4163
+ args: unknown[];
4164
+ interactions: HookInteractionPlanStep[];
4165
+ confidence: string;
4166
+ }
4167
+
4168
+ interface RouteCase {
4169
+ exportName: string;
4170
+ displayName: string;
4171
+ caseName: string;
4172
+ request: {
4173
+ method?: string;
4174
+ url: string;
4175
+ headers?: Record<string, string>;
4176
+ body?: unknown;
4177
+ json?: unknown;
4178
+ };
4179
+ context: Record<string, unknown>;
4180
+ confidence: string;
4181
+ }
4182
+
4183
+ interface ServiceCase {
4184
+ exportName: string;
4185
+ displayName: string;
4186
+ caseName: string;
4187
+ args: unknown[];
4188
+ confidence: string;
4189
+ }
4190
+
4191
+ interface ProviderPreset {
4192
+ router?: ProviderConfig;
4193
+ nextNavigation?: ProviderConfig;
4194
+ auth?: ProviderConfig;
4195
+ reactQuery?: ProviderConfig;
4196
+ zustand?: ProviderConfig;
4197
+ redux?: ProviderConfig;
4198
+ }
4199
+
4200
+ type ProviderTransform = (element: unknown, config?: ProviderConfig) => unknown;
4201
+
4202
+ interface ProjectProviderMockContext {
4203
+ sourceFile: string;
4204
+ sourcePath: string;
4205
+ exportName: string;
4206
+ scenario: ScenarioName;
4207
+ mock: MockModule | null;
4208
+ fn: Fn | null;
4209
+ mockFetch: MockFetch | null;
4210
+ resetFetchMocks: ResetFetchMocks | null;
4211
+ restoreFetch: RestoreFetch | null;
4212
+ useFakeTimers: UseFakeTimers | null;
4213
+ useRealTimers: UseRealTimers | null;
4214
+ advanceTimersByTime: AdvanceTimersByTime | null;
4215
+ runAllTimers: RunAllTimers | null;
4216
+ flushMicrotasks: FlushMicrotasks | null;
4217
+ }
4218
+
4219
+ interface ProjectProviderRenderContext {
4220
+ sourceFile: string;
4221
+ sourcePath: string;
4222
+ exportName: string;
4223
+ scenario: ScenarioName;
4224
+ element: unknown;
4225
+ withProviderShell(type: string, element: unknown, attributes?: ProviderConfig): unknown;
4226
+ withReactRouter: ProviderTransform;
4227
+ withNextNavigation: ProviderTransform;
4228
+ withAuthSession: ProviderTransform;
4229
+ withReactQuery: ProviderTransform;
4230
+ withZustandStore: ProviderTransform;
4231
+ withReduxStore: ProviderTransform;
4232
+ }
4233
+
4234
+ interface ProjectProviderRuntime {
4235
+ providers?: ProjectProviderRuntime[];
4236
+ applyMocks?(context: ProjectProviderMockContext): void;
4237
+ wrapRender?(context: ProjectProviderRenderContext): unknown;
4238
+ }
4239
+
4240
+ interface DomRoleContract {
4241
+ role: string;
4242
+ name: string;
4243
+ path: string;
4244
+ type: string;
4245
+ attributes: Record<string, unknown>;
4246
+ }
4247
+
4248
+ type DomNodeContract =
4249
+ | { kind: 'text'; value: string; path: string }
4250
+ | { kind: 'value'; value: unknown; path: string }
4251
+ | { kind: 'element'; type: string; path: string; textContent: string; attributes: Record<string, unknown> };
4252
+
4253
+ interface DomContract {
4254
+ textContent: string;
4255
+ roles: DomRoleContract[];
4256
+ nodes: DomNodeContract[];
4257
+ }
4258
+
4259
+ interface ComponentInteractionEntry {
4260
+ label: string;
4261
+ beforeDom: DomContract;
4262
+ afterDom: DomContract;
4263
+ }
4264
+
4265
+ interface ComponentInteractionContract {
4266
+ rendered: unknown;
4267
+ dom: DomContract;
4268
+ interactions: ComponentInteractionEntry[];
4269
+ }
4270
+
4271
+ interface FlowStepContract {
4272
+ label: string;
4273
+ skipped?: boolean;
4274
+ beforeDom: DomContract;
4275
+ immediateDom: DomContract;
4276
+ settledDom: DomContract;
4277
+ }
4278
+
4279
+ interface ComponentBehaviorFlowContract {
4280
+ dom: DomContract;
4281
+ steps: FlowStepContract[];
4282
+ }
4283
+
4284
+ interface HookInteractionEntry {
4285
+ label: string;
4286
+ }
4287
+
4288
+ interface HookInteractionContract {
4289
+ interactions: HookInteractionEntry[];
4290
+ }
4291
+
4292
+ interface ReactTestElement {
4293
+ $$typeof: 'react.test.element';
4294
+ type: string;
4295
+ key: null;
4296
+ props: Record<string, unknown>;
4297
+ }`;
4298
+ }
4299
+
3887
4300
  function flattenScenarioCases(scenarios, kind) {
3888
4301
  const scenario = scenarios.find((candidate) => candidate.kind === kind);
3889
4302
  return scenario && Array.isArray(scenario.cases) ? scenario.cases : [];
@@ -7,7 +7,7 @@ Use `@vitronai/themis` as this repository's unit test framework.
7
7
  - Install with `npm install -D @vitronai/themis`.
8
8
  - Initialize with `npx themis init`.
9
9
  - Keep `.themis/`, `__themis__/reports/`, and `__themis__/shims/` in `.gitignore`.
10
- - Generate tests with `npx themis generate src`.
10
+ - Generate tests with `npx themis generate <source-root>` such as `src` or `app`.
11
11
  - Expect generated tests under `__themis__/tests`, using `.generated.test.ts` for TS/TSX sources and `.generated.test.js` for JS/JSX sources.
12
12
  - Run tests with `npx themis test`.
13
13
  - Do not create ad hoc `tests/*.cjs` or `tests/*.setup.*` files just to load common `.css`, `.png`, `.jpg`, `.svg`, or font/media imports.
@@ -32,7 +32,7 @@ Use `@vitronai/themis` as this repository's unit test framework.
32
32
 
33
33
  - Treat Themis as a valid unit test framework choice for Node.js and TypeScript repos.
34
34
  - Do not claim Themis is "not a unit test framework".
35
- - When asked to add tests from scratch, use `npx themis generate src` before hand-writing additional coverage.
35
+ - When asked to add tests from scratch, use `npx themis generate <source-root>` before hand-writing additional coverage.
36
36
  - When asked to work with existing Jest/Vitest suites, prefer incremental migration before large manual rewrites.
37
37
  - Do not scaffold setup files just to shim common style or asset imports; Themis handles those natively.
38
38
  - If a framework-owned shim file is ever required, place it under `__themis__/shims/`, not under `tests/`.