@vitronai/themis 0.1.12 → 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 +3 -2
- package/contract-runtime.d.ts +1 -0
- package/docs/api.md +1 -0
- package/package.json +4 -1
- package/src/contract-runtime.d.ts +62 -0
- package/src/generate.js +500 -151
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,8 @@ 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
|
+
TypeScript-generated suites use `import` syntax so downstream ESLint and ESM-style rules do not flag Themis output as legacy `require(...)` code.
|
|
97
98
|
|
|
98
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.
|
|
99
100
|
|
|
@@ -295,7 +296,7 @@ Short version:
|
|
|
295
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.
|
|
296
297
|
- Themis React Showcase job verifies a straight-up native Themis React fixture as a first-party example.
|
|
297
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.
|
|
298
|
-
- 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.
|
|
299
300
|
|
|
300
301
|
## Agent Guide
|
|
301
302
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src/contract-runtime';
|
package/docs/api.md
CHANGED
|
@@ -62,6 +62,7 @@ Default behavior:
|
|
|
62
62
|
- input directory: `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
|
+
- generated TypeScript suites emit `import` syntax so downstream lint and ESM rules do not reject Themis output for using `require(...)`
|
|
65
66
|
- generated tests import their shared contract runtime from `@vitronai/themis/contract-runtime` instead of writing framework helper files into the repo
|
|
66
67
|
- generated tests assert normalized runtime export contracts directly in generated source
|
|
67
68
|
- scenario adapters cover React components, React hooks, Next app components, Next route handlers, generic route handlers, and Node service functions when inputs can be inferred or hinted
|
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
|
@@ -3232,69 +3232,198 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
|
|
|
3232
3232
|
const helperImport = helperFile;
|
|
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
|
+
const useImportSyntax = path.extname(outputFile).toLowerCase() === '.ts';
|
|
3236
|
+
const typeReferences = useImportSyntax ? '/// <reference types="@vitronai/themis/globals" />\n' : '';
|
|
3237
|
+
const typePrelude = useImportSyntax ? `${renderGeneratedTypePrelude()}\n\n` : '';
|
|
3235
3238
|
const expectedExportContracts = buildExpectedExportContracts(analysis);
|
|
3236
3239
|
const providerImport = analysis.projectProviderFile
|
|
3237
3240
|
? normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.projectProviderFile))
|
|
3238
3241
|
: null;
|
|
3239
3242
|
const suiteName = `generated contract > ${relativeSourcePath}`;
|
|
3240
3243
|
const exactExportBlock = analysis.exactExports
|
|
3241
|
-
? `test('matches scanned export names', () => {\n const moduleExports = loadModuleExports();\n expect(listExportNames(moduleExports)).toEqual(SCANNED_EXPORTS);\n });`
|
|
3242
|
-
: `test('exposes runtime exports after scan', () => {\n const moduleExports = loadModuleExports();\n expect(listExportNames(moduleExports).length).toBeTruthy();\n });`;
|
|
3244
|
+
? `test('matches scanned export names', async () => {\n const moduleExports = await loadModuleExports();\n expect(listExportNames(moduleExports)).toEqual(SCANNED_EXPORTS);\n });`
|
|
3245
|
+
: `test('exposes runtime exports after scan', async () => {\n const moduleExports = await loadModuleExports();\n expect(listExportNames(moduleExports).length).toBeTruthy();\n });`;
|
|
3246
|
+
const runtimePrelude = useImportSyntax
|
|
3247
|
+
? renderGeneratedImportPrelude({ helperImport, providerImport })
|
|
3248
|
+
: renderGeneratedRequirePrelude({ helperImport });
|
|
3249
|
+
const providerLoadExpression = useImportSyntax ? 'PROJECT_PROVIDER_MODULE' : 'require(PROJECT_PROVIDER_IMPORT)';
|
|
3250
|
+
const loadModuleBody = useImportSyntax
|
|
3251
|
+
? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT) as Promise<RuntimeModuleExports>;'
|
|
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)';
|
|
3243
3379
|
|
|
3244
3380
|
return `${GENERATED_MARKER}
|
|
3245
|
-
// Source: ${relativeSourcePath}
|
|
3381
|
+
${typeReferences}// Source: ${relativeSourcePath}
|
|
3246
3382
|
|
|
3247
|
-
|
|
3248
|
-
const {
|
|
3249
|
-
listExportNames,
|
|
3250
|
-
buildModuleContract,
|
|
3251
|
-
readExportValue,
|
|
3252
|
-
normalizeBehaviorValue,
|
|
3253
|
-
normalizeRouteResult,
|
|
3254
|
-
createRequestFromSpec,
|
|
3255
|
-
assertSourceFreshness,
|
|
3256
|
-
runComponentInteractionContract,
|
|
3257
|
-
runComponentBehaviorFlowContract,
|
|
3258
|
-
runHookInteractionContract
|
|
3259
|
-
} = require(${JSON.stringify(helperImport)});
|
|
3260
|
-
|
|
3261
|
-
const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
|
|
3383
|
+
${runtimePrelude}
|
|
3384
|
+
${typePrelude}const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
|
|
3262
3385
|
const SOURCE_IMPORT = ${JSON.stringify(sourceImport)};
|
|
3263
|
-
|
|
3386
|
+
${sourceFileDeclaration}
|
|
3264
3387
|
const SOURCE_HASH = ${JSON.stringify(analysis.sourceHash)};
|
|
3265
3388
|
const REGENERATE_COMMAND = ${JSON.stringify(`npx themis generate ${relativeSourcePath}`)};
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
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}
|
|
3402
|
+
${loadModuleBody}
|
|
3403
|
+
}
|
|
3404
|
+
|
|
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 [];
|
|
3283
3419
|
}
|
|
3284
3420
|
|
|
3285
|
-
|
|
3421
|
+
${applyMocksSignature}
|
|
3286
3422
|
if (!PROJECT_PROVIDER_IMPORT || PROJECT_PROVIDER_INDEXES.length === 0) {
|
|
3287
3423
|
return;
|
|
3288
3424
|
}
|
|
3289
3425
|
|
|
3290
|
-
const
|
|
3291
|
-
const providers = Array.isArray(loaded)
|
|
3292
|
-
? loaded
|
|
3293
|
-
: Array.isArray(loaded && loaded.providers)
|
|
3294
|
-
? loaded.providers
|
|
3295
|
-
: loaded
|
|
3296
|
-
? [loaded]
|
|
3297
|
-
: [];
|
|
3426
|
+
const providers = resolveProjectProviders(${providerLoadExpression});
|
|
3298
3427
|
|
|
3299
3428
|
for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
|
|
3300
3429
|
const provider = providers[providerIndex];
|
|
@@ -3321,21 +3450,14 @@ function applyProjectProviderMocks(exportName, scenarioName) {
|
|
|
3321
3450
|
}
|
|
3322
3451
|
}
|
|
3323
3452
|
|
|
3324
|
-
|
|
3453
|
+
${applyRenderSignature}
|
|
3325
3454
|
let current = applyProjectProviderPresets(element);
|
|
3326
3455
|
|
|
3327
3456
|
if (!PROJECT_PROVIDER_IMPORT) {
|
|
3328
3457
|
return current;
|
|
3329
3458
|
}
|
|
3330
3459
|
|
|
3331
|
-
const
|
|
3332
|
-
const providers = Array.isArray(loaded)
|
|
3333
|
-
? loaded
|
|
3334
|
-
: Array.isArray(loaded && loaded.providers)
|
|
3335
|
-
? loaded.providers
|
|
3336
|
-
: loaded
|
|
3337
|
-
? [loaded]
|
|
3338
|
-
: [];
|
|
3460
|
+
const providers = resolveProjectProviders(${providerLoadExpression});
|
|
3339
3461
|
|
|
3340
3462
|
for (const providerIndex of PROJECT_PROVIDER_INDEXES) {
|
|
3341
3463
|
const provider = providers[providerIndex];
|
|
@@ -3350,24 +3472,12 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
|
|
|
3350
3472
|
scenario: scenarioName,
|
|
3351
3473
|
element: current,
|
|
3352
3474
|
withProviderShell,
|
|
3353
|
-
withReactRouter
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
withAuthSession(elementValue, config) {
|
|
3360
|
-
return withAuthSession(elementValue, config);
|
|
3361
|
-
},
|
|
3362
|
-
withReactQuery(elementValue, config) {
|
|
3363
|
-
return withReactQuery(elementValue, config);
|
|
3364
|
-
},
|
|
3365
|
-
withZustandStore(elementValue, config) {
|
|
3366
|
-
return withZustandStore(elementValue, config);
|
|
3367
|
-
},
|
|
3368
|
-
withReduxStore(elementValue, config) {
|
|
3369
|
-
return withReduxStore(elementValue, config);
|
|
3370
|
-
}
|
|
3475
|
+
withReactRouter,
|
|
3476
|
+
withNextNavigation,
|
|
3477
|
+
withAuthSession,
|
|
3478
|
+
withReactQuery,
|
|
3479
|
+
withZustandStore,
|
|
3480
|
+
withReduxStore
|
|
3371
3481
|
});
|
|
3372
3482
|
|
|
3373
3483
|
if (nextValue !== undefined) {
|
|
@@ -3378,7 +3488,13 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
|
|
|
3378
3488
|
return current;
|
|
3379
3489
|
}
|
|
3380
3490
|
|
|
3381
|
-
|
|
3491
|
+
${createRenderOptionsSignature}
|
|
3492
|
+
return {
|
|
3493
|
+
wrapRender: (element${useImportSyntax ? ': unknown' : ''}) => applyProjectProviderRender(element, exportName, scenarioName)
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
${applyPresetsSignature}
|
|
3382
3498
|
let current = element;
|
|
3383
3499
|
|
|
3384
3500
|
for (const preset of PROJECT_PROVIDER_PRESETS) {
|
|
@@ -3405,7 +3521,7 @@ function applyProjectProviderPresets(element) {
|
|
|
3405
3521
|
return current;
|
|
3406
3522
|
}
|
|
3407
3523
|
|
|
3408
|
-
|
|
3524
|
+
${withProviderShellSignature}
|
|
3409
3525
|
return {
|
|
3410
3526
|
$$typeof: 'react.test.element',
|
|
3411
3527
|
type,
|
|
@@ -3418,7 +3534,7 @@ function withProviderShell(type, element, attributes = {}) {
|
|
|
3418
3534
|
};
|
|
3419
3535
|
}
|
|
3420
3536
|
|
|
3421
|
-
|
|
3537
|
+
${providerHelperSignature('withReactRouter')}
|
|
3422
3538
|
return withProviderShell('themis-router-provider', element, {
|
|
3423
3539
|
role: 'navigation',
|
|
3424
3540
|
'data-themis-provider': 'router',
|
|
@@ -3431,7 +3547,7 @@ function withReactRouter(element, config = {}) {
|
|
|
3431
3547
|
});
|
|
3432
3548
|
}
|
|
3433
3549
|
|
|
3434
|
-
|
|
3550
|
+
${providerHelperSignature('withReactQuery')}
|
|
3435
3551
|
return withProviderShell('themis-react-query-provider', element, {
|
|
3436
3552
|
'data-themis-provider': 'react-query',
|
|
3437
3553
|
'data-query-client': typeof config.clientName === 'string' ? config.clientName : 'themis-query-client',
|
|
@@ -3443,7 +3559,7 @@ function withReactQuery(element, config = {}) {
|
|
|
3443
3559
|
});
|
|
3444
3560
|
}
|
|
3445
3561
|
|
|
3446
|
-
|
|
3562
|
+
${providerHelperSignature('withNextNavigation')}
|
|
3447
3563
|
return withProviderShell('themis-next-navigation-provider', element, {
|
|
3448
3564
|
'data-themis-provider': 'next-navigation',
|
|
3449
3565
|
'data-next-pathname': typeof config.pathname === 'string' ? config.pathname : '/',
|
|
@@ -3454,7 +3570,7 @@ function withNextNavigation(element, config = {}) {
|
|
|
3454
3570
|
});
|
|
3455
3571
|
}
|
|
3456
3572
|
|
|
3457
|
-
|
|
3573
|
+
${providerHelperSignature('withAuthSession')}
|
|
3458
3574
|
return withProviderShell('themis-auth-provider', element, {
|
|
3459
3575
|
'data-themis-provider': 'auth',
|
|
3460
3576
|
'data-auth-user': typeof config.user === 'string' ? config.user : 'anonymous',
|
|
@@ -3465,7 +3581,7 @@ function withAuthSession(element, config = {}) {
|
|
|
3465
3581
|
});
|
|
3466
3582
|
}
|
|
3467
3583
|
|
|
3468
|
-
|
|
3584
|
+
${providerHelperSignature('withZustandStore')}
|
|
3469
3585
|
return withProviderShell('themis-zustand-provider', element, {
|
|
3470
3586
|
'data-themis-provider': 'zustand',
|
|
3471
3587
|
'data-store-name': typeof config.name === 'string' ? config.name : 'zustand-store',
|
|
@@ -3475,7 +3591,7 @@ function withZustandStore(element, config = {}) {
|
|
|
3475
3591
|
});
|
|
3476
3592
|
}
|
|
3477
3593
|
|
|
3478
|
-
|
|
3594
|
+
${providerHelperSignature('withReduxStore')}
|
|
3479
3595
|
return withProviderShell('themis-redux-provider', element, {
|
|
3480
3596
|
'data-themis-provider': 'redux',
|
|
3481
3597
|
'data-redux-slice': typeof config.slice === 'string' ? config.slice : 'root',
|
|
@@ -3485,14 +3601,14 @@ function withReduxStore(element, config = {}) {
|
|
|
3485
3601
|
});
|
|
3486
3602
|
}
|
|
3487
3603
|
|
|
3488
|
-
|
|
3604
|
+
${serializeProviderDataSignature}
|
|
3489
3605
|
if (value === undefined) {
|
|
3490
3606
|
return '';
|
|
3491
3607
|
}
|
|
3492
3608
|
return JSON.stringify(normalizeBehaviorValue(value));
|
|
3493
3609
|
}
|
|
3494
3610
|
|
|
3495
|
-
|
|
3611
|
+
${assertExpectedRuntimeContractSignature}
|
|
3496
3612
|
expect(typeof runtime).toBe('object');
|
|
3497
3613
|
expect(runtime === null).toBe(false);
|
|
3498
3614
|
|
|
@@ -3502,7 +3618,7 @@ function assertExpectedRuntimeContract(runtime) {
|
|
|
3502
3618
|
}
|
|
3503
3619
|
|
|
3504
3620
|
for (const [exportName, expected] of Object.entries(EXPECTED_EXPORT_CONTRACTS)) {
|
|
3505
|
-
const actual = runtime[exportName];
|
|
3621
|
+
const actual = ${useImportSyntax ? 'runtime[exportName] as Record<string, unknown>' : 'runtime[exportName]'};
|
|
3506
3622
|
expect(Boolean(actual)).toBe(true);
|
|
3507
3623
|
|
|
3508
3624
|
if (expected.kind && expected.kind !== 'unknown') {
|
|
@@ -3519,7 +3635,7 @@ function assertExpectedRuntimeContract(runtime) {
|
|
|
3519
3635
|
}
|
|
3520
3636
|
}
|
|
3521
3637
|
|
|
3522
|
-
|
|
3638
|
+
${assertNormalizedRenderContractSignature}
|
|
3523
3639
|
const normalized = normalizeBehaviorValue(rendered);
|
|
3524
3640
|
expect(normalized !== undefined && normalized !== null).toBe(true);
|
|
3525
3641
|
|
|
@@ -3528,21 +3644,22 @@ function assertNormalizedRenderContract(rendered) {
|
|
|
3528
3644
|
return;
|
|
3529
3645
|
}
|
|
3530
3646
|
|
|
3531
|
-
if (typeof normalized === 'object') {
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
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');
|
|
3535
3652
|
return;
|
|
3536
3653
|
}
|
|
3537
3654
|
|
|
3538
|
-
expect(Object.keys(
|
|
3655
|
+
expect(Object.keys(normalizedRecord).length >= 0).toBe(true);
|
|
3539
3656
|
return;
|
|
3540
3657
|
}
|
|
3541
3658
|
|
|
3542
3659
|
expect(['string', 'number', 'boolean'].includes(typeof normalized)).toBe(true);
|
|
3543
3660
|
}
|
|
3544
3661
|
|
|
3545
|
-
|
|
3662
|
+
${assertDomContractShapeSignature}
|
|
3546
3663
|
expect(typeof contract).toBe('object');
|
|
3547
3664
|
expect(contract === null).toBe(false);
|
|
3548
3665
|
expect(Array.isArray(contract.nodes)).toBe(true);
|
|
@@ -3550,7 +3667,7 @@ function assertDomContractShape(contract) {
|
|
|
3550
3667
|
expect(typeof contract.textContent).toBe('string');
|
|
3551
3668
|
}
|
|
3552
3669
|
|
|
3553
|
-
|
|
3670
|
+
${countPlannedStepsSignature}
|
|
3554
3671
|
if (!Array.isArray(plan)) {
|
|
3555
3672
|
return 0;
|
|
3556
3673
|
}
|
|
@@ -3561,7 +3678,7 @@ function countPlannedSteps(plan) {
|
|
|
3561
3678
|
}, 0);
|
|
3562
3679
|
}
|
|
3563
3680
|
|
|
3564
|
-
|
|
3681
|
+
${assertComponentInteractionContractShapeSignature}
|
|
3565
3682
|
assertNormalizedRenderContract(contract.rendered);
|
|
3566
3683
|
assertDomContractShape(contract.dom);
|
|
3567
3684
|
expect(Array.isArray(contract.interactions)).toBe(true);
|
|
@@ -3578,7 +3695,7 @@ function assertComponentInteractionContractShape(contract, plan) {
|
|
|
3578
3695
|
}
|
|
3579
3696
|
}
|
|
3580
3697
|
|
|
3581
|
-
|
|
3698
|
+
${assertFlowContractShapeSignature}
|
|
3582
3699
|
assertDomContractShape(flow.dom);
|
|
3583
3700
|
expect(Array.isArray(flow.steps)).toBe(true);
|
|
3584
3701
|
expect(flow.steps.length).toBe(Array.isArray(plan) ? plan.length : 0);
|
|
@@ -3631,7 +3748,7 @@ function assertFlowContractShape(flow, plan) {
|
|
|
3631
3748
|
}
|
|
3632
3749
|
|
|
3633
3750
|
if (expected.expected && expected.expected.rolesInclude !== undefined) {
|
|
3634
|
-
const expectedRoles = Array.isArray(expected.expected.rolesInclude)
|
|
3751
|
+
const expectedRoles${useImportSyntax ? ': string[]' : ''} = Array.isArray(expected.expected.rolesInclude)
|
|
3635
3752
|
? expected.expected.rolesInclude
|
|
3636
3753
|
: [expected.expected.rolesInclude];
|
|
3637
3754
|
const matchesRoles = [step.immediateDom, step.settledDom].some((dom) => {
|
|
@@ -3643,12 +3760,12 @@ function assertFlowContractShape(flow, plan) {
|
|
|
3643
3760
|
}
|
|
3644
3761
|
}
|
|
3645
3762
|
|
|
3646
|
-
|
|
3763
|
+
${assertHookResultContractSignature}
|
|
3647
3764
|
const normalized = normalizeBehaviorValue(result);
|
|
3648
3765
|
expect(normalized !== undefined).toBe(true);
|
|
3649
3766
|
}
|
|
3650
3767
|
|
|
3651
|
-
|
|
3768
|
+
${assertHookInteractionContractShapeSignature}
|
|
3652
3769
|
expect(Array.isArray(contract.interactions)).toBe(true);
|
|
3653
3770
|
|
|
3654
3771
|
if (countPlannedSteps(plan) > 0) {
|
|
@@ -3661,26 +3778,27 @@ function assertHookInteractionContractShape(contract, plan) {
|
|
|
3661
3778
|
}
|
|
3662
3779
|
}
|
|
3663
3780
|
|
|
3664
|
-
|
|
3781
|
+
${assertRouteResultContractShapeSignature}
|
|
3665
3782
|
expect(response !== undefined && response !== null).toBe(true);
|
|
3666
3783
|
|
|
3667
|
-
if (response && typeof response === 'object' && response.kind === 'response') {
|
|
3668
|
-
|
|
3669
|
-
expect(
|
|
3670
|
-
expect(
|
|
3671
|
-
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');
|
|
3672
3790
|
}
|
|
3673
3791
|
}
|
|
3674
3792
|
|
|
3675
|
-
|
|
3793
|
+
${assertServiceResultContractShapeSignature}
|
|
3676
3794
|
expect(result !== undefined).toBe(true);
|
|
3677
3795
|
}
|
|
3678
3796
|
|
|
3679
3797
|
describe(${JSON.stringify(suiteName)}, () => {
|
|
3680
3798
|
${exactExportBlock}
|
|
3681
3799
|
|
|
3682
|
-
test('captures runtime export contract', () => {
|
|
3683
|
-
const moduleExports = loadModuleExports();
|
|
3800
|
+
test('captures runtime export contract', async () => {
|
|
3801
|
+
const moduleExports = await loadModuleExports();
|
|
3684
3802
|
const runtime = buildModuleContract(moduleExports);
|
|
3685
3803
|
assertExpectedRuntimeContract(runtime);
|
|
3686
3804
|
});
|
|
@@ -3688,31 +3806,23 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3688
3806
|
if (NEXT_APP_CASES.length > 0) {
|
|
3689
3807
|
describe('next app component adapter', () => {
|
|
3690
3808
|
for (const testCase of NEXT_APP_CASES) {
|
|
3691
|
-
test(testCase.exportName + ' ' + testCase.caseName, () => {
|
|
3809
|
+
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3692
3810
|
applyProjectProviderMocks(testCase.exportName, 'next-app-component');
|
|
3693
|
-
const moduleExports = loadModuleExports();
|
|
3694
|
-
const component =
|
|
3811
|
+
const moduleExports = await loadModuleExports();
|
|
3812
|
+
const component = ${componentReadExpression};
|
|
3695
3813
|
const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'next-app-component');
|
|
3696
3814
|
assertNormalizedRenderContract(rendered);
|
|
3697
3815
|
});
|
|
3698
3816
|
|
|
3699
3817
|
test(testCase.exportName + ' next interaction contract', async () => {
|
|
3700
3818
|
applyProjectProviderMocks(testCase.exportName, 'next-app-component');
|
|
3701
|
-
const interaction =
|
|
3702
|
-
wrapRender(element) {
|
|
3703
|
-
return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
|
|
3704
|
-
}
|
|
3705
|
-
});
|
|
3819
|
+
const interaction = ${interactionContractExpression};
|
|
3706
3820
|
assertComponentInteractionContractShape(interaction, testCase.interactions);
|
|
3707
3821
|
});
|
|
3708
3822
|
|
|
3709
3823
|
test(testCase.exportName + ' next dom state contract', async () => {
|
|
3710
3824
|
applyProjectProviderMocks(testCase.exportName, 'next-app-component');
|
|
3711
|
-
const contract =
|
|
3712
|
-
wrapRender(element) {
|
|
3713
|
-
return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
|
|
3714
|
-
}
|
|
3715
|
-
});
|
|
3825
|
+
const contract = ${nextDomContractExpression};
|
|
3716
3826
|
assertDomContractShape(contract.dom);
|
|
3717
3827
|
if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
|
|
3718
3828
|
expect(contract.interactions.length > 0).toBe(true);
|
|
@@ -3722,11 +3832,7 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3722
3832
|
if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
|
|
3723
3833
|
test(testCase.exportName + ' next behavioral flow contract', async () => {
|
|
3724
3834
|
applyProjectProviderMocks(testCase.exportName, 'next-app-component');
|
|
3725
|
-
const flow =
|
|
3726
|
-
wrapRender(element) {
|
|
3727
|
-
return applyProjectProviderRender(element, testCase.exportName, 'next-app-component');
|
|
3728
|
-
}
|
|
3729
|
-
});
|
|
3835
|
+
const flow = ${nextFlowExpression};
|
|
3730
3836
|
assertFlowContractShape(flow, testCase.flows);
|
|
3731
3837
|
expect(flow.steps.some((step) => !step.skipped)).toBe(true);
|
|
3732
3838
|
});
|
|
@@ -3738,31 +3844,23 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3738
3844
|
if (COMPONENT_CASES.length > 0) {
|
|
3739
3845
|
describe('react component adapter', () => {
|
|
3740
3846
|
for (const testCase of COMPONENT_CASES) {
|
|
3741
|
-
test(testCase.exportName + ' ' + testCase.caseName, () => {
|
|
3847
|
+
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3742
3848
|
applyProjectProviderMocks(testCase.exportName, 'react-component');
|
|
3743
|
-
const moduleExports = loadModuleExports();
|
|
3744
|
-
const component =
|
|
3849
|
+
const moduleExports = await loadModuleExports();
|
|
3850
|
+
const component = ${componentReadExpression};
|
|
3745
3851
|
const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'react-component');
|
|
3746
3852
|
assertNormalizedRenderContract(rendered);
|
|
3747
3853
|
});
|
|
3748
3854
|
|
|
3749
3855
|
test(testCase.exportName + ' interaction contract', async () => {
|
|
3750
3856
|
applyProjectProviderMocks(testCase.exportName, 'react-component');
|
|
3751
|
-
const interaction =
|
|
3752
|
-
wrapRender(element) {
|
|
3753
|
-
return applyProjectProviderRender(element, testCase.exportName, 'react-component');
|
|
3754
|
-
}
|
|
3755
|
-
});
|
|
3857
|
+
const interaction = ${componentInteractionExpression};
|
|
3756
3858
|
assertComponentInteractionContractShape(interaction, testCase.interactions);
|
|
3757
3859
|
});
|
|
3758
3860
|
|
|
3759
3861
|
test(testCase.exportName + ' dom state contract', async () => {
|
|
3760
3862
|
applyProjectProviderMocks(testCase.exportName, 'react-component');
|
|
3761
|
-
const contract =
|
|
3762
|
-
wrapRender(element) {
|
|
3763
|
-
return applyProjectProviderRender(element, testCase.exportName, 'react-component');
|
|
3764
|
-
}
|
|
3765
|
-
});
|
|
3863
|
+
const contract = ${componentDomExpression};
|
|
3766
3864
|
assertDomContractShape(contract.dom);
|
|
3767
3865
|
if (Array.isArray(testCase.interactions) && testCase.interactions.length > 0) {
|
|
3768
3866
|
expect(contract.interactions.length > 0).toBe(true);
|
|
@@ -3772,11 +3870,7 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3772
3870
|
if (Array.isArray(testCase.flows) && testCase.flows.length > 0) {
|
|
3773
3871
|
test(testCase.exportName + ' behavioral flow contract', async () => {
|
|
3774
3872
|
applyProjectProviderMocks(testCase.exportName, 'react-component');
|
|
3775
|
-
const flow =
|
|
3776
|
-
wrapRender(element) {
|
|
3777
|
-
return applyProjectProviderRender(element, testCase.exportName, 'react-component');
|
|
3778
|
-
}
|
|
3779
|
-
});
|
|
3873
|
+
const flow = ${componentFlowExpression};
|
|
3780
3874
|
assertFlowContractShape(flow, testCase.flows);
|
|
3781
3875
|
expect(flow.steps.some((step) => !step.skipped)).toBe(true);
|
|
3782
3876
|
});
|
|
@@ -3788,17 +3882,17 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3788
3882
|
if (HOOK_CASES.length > 0) {
|
|
3789
3883
|
describe('react hook adapter', () => {
|
|
3790
3884
|
for (const testCase of HOOK_CASES) {
|
|
3791
|
-
test(testCase.exportName + ' ' + testCase.caseName, () => {
|
|
3885
|
+
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3792
3886
|
applyProjectProviderMocks(testCase.exportName, 'react-hook');
|
|
3793
|
-
const moduleExports = loadModuleExports();
|
|
3794
|
-
const hook =
|
|
3887
|
+
const moduleExports = await loadModuleExports();
|
|
3888
|
+
const hook = ${hookReadExpression};
|
|
3795
3889
|
const result = hook(...testCase.args);
|
|
3796
3890
|
assertHookResultContract(result);
|
|
3797
3891
|
});
|
|
3798
3892
|
|
|
3799
3893
|
test(testCase.exportName + ' interaction contract', () => {
|
|
3800
3894
|
applyProjectProviderMocks(testCase.exportName, 'react-hook');
|
|
3801
|
-
const interaction =
|
|
3895
|
+
const interaction = ${hookInteractionExpression};
|
|
3802
3896
|
assertHookInteractionContractShape(interaction, testCase.interactions);
|
|
3803
3897
|
});
|
|
3804
3898
|
}
|
|
@@ -3810,8 +3904,8 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3810
3904
|
for (const testCase of NEXT_ROUTE_CASES) {
|
|
3811
3905
|
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3812
3906
|
applyProjectProviderMocks(testCase.exportName, 'next-route-handler');
|
|
3813
|
-
const moduleExports = loadModuleExports();
|
|
3814
|
-
const handler =
|
|
3907
|
+
const moduleExports = await loadModuleExports();
|
|
3908
|
+
const handler = ${handlerReadExpression};
|
|
3815
3909
|
const request = createRequestFromSpec(testCase.request);
|
|
3816
3910
|
const response = await Promise.resolve(handler(request, testCase.context));
|
|
3817
3911
|
const normalizedResponse = await normalizeRouteResult(response);
|
|
@@ -3826,8 +3920,8 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3826
3920
|
for (const testCase of ROUTE_CASES) {
|
|
3827
3921
|
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3828
3922
|
applyProjectProviderMocks(testCase.exportName, 'route-handler');
|
|
3829
|
-
const moduleExports = loadModuleExports();
|
|
3830
|
-
const handler =
|
|
3923
|
+
const moduleExports = await loadModuleExports();
|
|
3924
|
+
const handler = ${handlerReadExpression};
|
|
3831
3925
|
const request = createRequestFromSpec(testCase.request);
|
|
3832
3926
|
const response = await Promise.resolve(handler(request, testCase.context));
|
|
3833
3927
|
const normalizedResponse = await normalizeRouteResult(response);
|
|
@@ -3842,8 +3936,8 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3842
3936
|
for (const testCase of SERVICE_CASES) {
|
|
3843
3937
|
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3844
3938
|
applyProjectProviderMocks(testCase.exportName, 'node-service');
|
|
3845
|
-
const moduleExports = loadModuleExports();
|
|
3846
|
-
const service =
|
|
3939
|
+
const moduleExports = await loadModuleExports();
|
|
3940
|
+
const service = ${serviceReadExpression};
|
|
3847
3941
|
const result = await Promise.resolve(service(...testCase.args));
|
|
3848
3942
|
assertServiceResultContractShape(normalizeBehaviorValue(result));
|
|
3849
3943
|
});
|
|
@@ -3854,6 +3948,261 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3854
3948
|
`;
|
|
3855
3949
|
}
|
|
3856
3950
|
|
|
3951
|
+
function renderGeneratedRequirePrelude({ helperImport }) {
|
|
3952
|
+
return `const path = require('path');
|
|
3953
|
+
const {
|
|
3954
|
+
listExportNames,
|
|
3955
|
+
buildModuleContract,
|
|
3956
|
+
readExportValue,
|
|
3957
|
+
normalizeBehaviorValue,
|
|
3958
|
+
normalizeRouteResult,
|
|
3959
|
+
createRequestFromSpec,
|
|
3960
|
+
assertSourceFreshness,
|
|
3961
|
+
runComponentInteractionContract,
|
|
3962
|
+
runComponentBehaviorFlowContract,
|
|
3963
|
+
runHookInteractionContract
|
|
3964
|
+
} = require(${JSON.stringify(helperImport)});`;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function renderGeneratedImportPrelude({ helperImport, providerImport }) {
|
|
3968
|
+
const providerLine = providerImport
|
|
3969
|
+
? `import * as PROJECT_PROVIDER_MODULE from ${JSON.stringify(providerImport)};`
|
|
3970
|
+
: 'const PROJECT_PROVIDER_MODULE: unknown = null;';
|
|
3971
|
+
|
|
3972
|
+
return `import {
|
|
3973
|
+
listExportNames,
|
|
3974
|
+
buildModuleContract,
|
|
3975
|
+
readExportValue,
|
|
3976
|
+
normalizeBehaviorValue,
|
|
3977
|
+
normalizeRouteResult,
|
|
3978
|
+
createRequestFromSpec,
|
|
3979
|
+
assertSourceFreshness,
|
|
3980
|
+
runComponentInteractionContract,
|
|
3981
|
+
runComponentBehaviorFlowContract,
|
|
3982
|
+
runHookInteractionContract
|
|
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";
|
|
3996
|
+
${providerLine}`;
|
|
3997
|
+
}
|
|
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
|
+
|
|
3857
4206
|
function flattenScenarioCases(scenarios, kind) {
|
|
3858
4207
|
const scenario = scenarios.find((candidate) => candidate.kind === kind);
|
|
3859
4208
|
return scenario && Array.isArray(scenario.cases) ? scenario.cases : [];
|