@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 +9 -0
- package/README.md +5 -3
- package/contract-runtime.d.ts +1 -0
- package/docs/agents-adoption.md +3 -3
- package/docs/api.md +5 -3
- package/docs/vscode-extension.md +2 -0
- package/package.json +4 -1
- package/src/cli.js +1 -1
- package/src/contract-runtime.d.ts +62 -0
- package/src/generate.js +539 -126
- package/templates/AGENTS.themis.md +2 -2
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
|
|
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.
|
|
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';
|
package/docs/agents-adoption.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
14
|
+
npx themis generate <source-root>
|
|
15
15
|
npx themis test
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`npx themis generate
|
|
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 |
|
package/docs/vscode-extension.md
CHANGED
|
@@ -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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
3480
|
+
${sourceFileDeclaration}
|
|
3260
3481
|
const SOURCE_HASH = ${JSON.stringify(analysis.sourceHash)};
|
|
3261
3482
|
const REGENERATE_COMMAND = ${JSON.stringify(`npx themis generate ${relativeSourcePath}`)};
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3698
|
+
${serializeProviderDataSignature}
|
|
3482
3699
|
if (value === undefined) {
|
|
3483
3700
|
return '';
|
|
3484
3701
|
}
|
|
3485
3702
|
return JSON.stringify(normalizeBehaviorValue(value));
|
|
3486
3703
|
}
|
|
3487
3704
|
|
|
3488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3526
|
-
|
|
3527
|
-
expect(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3857
|
+
${assertHookResultContractSignature}
|
|
3640
3858
|
const normalized = normalizeBehaviorValue(result);
|
|
3641
3859
|
expect(normalized !== undefined).toBe(true);
|
|
3642
3860
|
}
|
|
3643
3861
|
|
|
3644
|
-
|
|
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
|
-
|
|
3875
|
+
${assertRouteResultContractShapeSignature}
|
|
3658
3876
|
expect(response !== undefined && response !== null).toBe(true);
|
|
3659
3877
|
|
|
3660
|
-
if (response && typeof response === 'object' && response.kind === 'response') {
|
|
3661
|
-
|
|
3662
|
-
expect(
|
|
3663
|
-
expect(
|
|
3664
|
-
expect(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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/`.
|