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