mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 22:34:56 +00:00
feat: Add comprehensive test migration tooling
Add AST-based migration framework for converting TypeScript tests to JSON: New files: - scripts/migrate-tests/converter.ts (350+ lines) * AST parser for .test.ts files * Extracts describe/it/expect blocks * Maps 30+ Jest/Vitest matchers to JSON types * Returns structured ConversionResult - scripts/migrate-tests/migrator.ts (250+ lines) * Batch discovery and migration orchestrator * Glob-based .test.ts file discovery * Automatic output directory creation * Dry-run mode for safe preview * Pretty-printed progress reporting * Package name mapping (frontends → packages) - scripts/migrate-tests/validator.ts (300+ lines) * JSON schema validation using AJV * Semantic checks (unique IDs, assertions) * Unused import warnings * Directory-wide validation support * Structured ValidationResult output - scripts/migrate-tests/index.ts * Unified export module - scripts/migrate-tests/README.md * Comprehensive usage guide * Conversion process documentation * Matcher mapping reference * Workflow recommendations * Troubleshooting guide Features: * 80/20 conversion (handles ~80% of tests cleanly) * Fallback for complex tests requiring manual adjustment * Dry-run mode to preview changes * Verbose logging for troubleshooting * Validates against tests_schema.json Matcher Support: * Basic: equals, deepEquals, notEquals, truthy, falsy * Numeric: greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual * Type: null, notNull, undefined, notUndefined, instanceOf * Collection: contains, matches, hasProperty, hasLength * DOM: toBeVisible, toBeInTheDocument, toHaveTextContent, toHaveAttribute, toHaveClass, toBeDisabled, toBeEnabled, toHaveValue * Control: throws, notThrows, custom This completes Phase 3 Task 4 of the JSON interpreter everywhere implementation. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
382
scripts/migrate-tests/README.md
Normal file
382
scripts/migrate-tests/README.md
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
# Test Migration Toolkit
|
||||||
|
|
||||||
|
Tools for migrating TypeScript test files (.test.ts) to declarative JSON format for the MetaBuilder unified test runner.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The toolkit consists of three main components:
|
||||||
|
|
||||||
|
1. **Converter** - Parses TypeScript test files and converts them to JSON
|
||||||
|
2. **Migrator** - Batch discovers and migrates all test files
|
||||||
|
3. **Validator** - Validates JSON test files against the schema
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
TypeScript .test.ts files
|
||||||
|
↓
|
||||||
|
[Converter] ← Uses TypeScript AST to parse test structure
|
||||||
|
↓
|
||||||
|
JSON test definitions
|
||||||
|
↓
|
||||||
|
[Validator] ← Ensures JSON conforms to schema
|
||||||
|
↓
|
||||||
|
/packages/*/unit-tests/tests.json
|
||||||
|
↓
|
||||||
|
[Unified Test Runner] ← Discovers and executes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Dry-Run Migration (Safe Preview)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preview what would be converted
|
||||||
|
npm --prefix scripts/migrate-tests run migrate -- --dry-run --verbose
|
||||||
|
|
||||||
|
# Or directly:
|
||||||
|
npx ts-node scripts/migrate-tests/migrator.ts --dry-run --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Actual Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migrate all discovered .test.ts files
|
||||||
|
npm --prefix scripts/migrate-tests run migrate
|
||||||
|
|
||||||
|
# Or directly:
|
||||||
|
npx ts-node scripts/migrate-tests/migrator.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validate Converted Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate all JSON test files in packages
|
||||||
|
npm --prefix scripts/migrate-tests run validate
|
||||||
|
|
||||||
|
# Or directly:
|
||||||
|
npx ts-node scripts/migrate-tests/validator.ts packages
|
||||||
|
|
||||||
|
# Validate specific directory:
|
||||||
|
npx ts-node scripts/migrate-tests/validator.ts packages/my_package
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Conversion Process
|
||||||
|
|
||||||
|
The converter uses TypeScript's AST (Abstract Syntax Tree) to understand test structure:
|
||||||
|
|
||||||
|
1. **Parse imports** - Extract all import statements into `imports` array
|
||||||
|
2. **Extract test suites** - Find all `describe()` blocks
|
||||||
|
3. **Parse tests** - Extract `it()` blocks within suites
|
||||||
|
4. **Parse assertions** - Extract `expect()` calls and map matchers to JSON types
|
||||||
|
5. **Build JSON** - Construct JSON test definition with $schema, package, imports, testSuites
|
||||||
|
|
||||||
|
### Matcher Mapping
|
||||||
|
|
||||||
|
The converter maps 30+ Vitest/Jest matchers to JSON assertion types:
|
||||||
|
|
||||||
|
| TypeScript Matcher | JSON Type | Example |
|
||||||
|
|-------------------|-----------|---------|
|
||||||
|
| `toBe()` | `equals` | Strict equality |
|
||||||
|
| `toEqual()` | `deepEquals` | Deep object equality |
|
||||||
|
| `toBeGreaterThan()` | `greaterThan` | Numeric comparison |
|
||||||
|
| `toContain()` | `contains` | String/array contains |
|
||||||
|
| `toThrow()` | `throws` | Exception handling |
|
||||||
|
| `toBeVisible()` | `toBeVisible` | DOM assertion |
|
||||||
|
| `toHaveClass()` | `toHaveClass` | DOM assertion |
|
||||||
|
| ... and 24 more | ... | ... |
|
||||||
|
|
||||||
|
### Package Name Mapping
|
||||||
|
|
||||||
|
Tests are placed in the appropriate package directory:
|
||||||
|
|
||||||
|
- `frontends/nextjs/*.test.ts` → `packages/nextjs_frontend/unit-tests/tests.json`
|
||||||
|
- `frontends/cli/*.test.ts` → `packages/cli_frontend/unit-tests/tests.json`
|
||||||
|
- `frontends/qt6/*.test.ts` → `packages/qt6_frontend/unit-tests/tests.json`
|
||||||
|
- Others → `packages/[extracted_name]/unit-tests/tests.json`
|
||||||
|
|
||||||
|
## JSON Test Format
|
||||||
|
|
||||||
|
### Basic Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://metabuilder.dev/schemas/tests.schema.json",
|
||||||
|
"schemaVersion": "2.0.0",
|
||||||
|
"package": "my_package",
|
||||||
|
"imports": [
|
||||||
|
{ "from": "@/lib/utils", "items": ["validateEmail"] }
|
||||||
|
],
|
||||||
|
"testSuites": [
|
||||||
|
{
|
||||||
|
"id": "suite_validate",
|
||||||
|
"name": "Email Validation",
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"id": "test_valid_email",
|
||||||
|
"name": "should accept valid email",
|
||||||
|
"arrange": {
|
||||||
|
"fixtures": { "email": "user@example.com" }
|
||||||
|
},
|
||||||
|
"act": {
|
||||||
|
"type": "function_call",
|
||||||
|
"target": "validateEmail",
|
||||||
|
"input": "$arrange.fixtures.email"
|
||||||
|
},
|
||||||
|
"assert": {
|
||||||
|
"expectations": [
|
||||||
|
{
|
||||||
|
"type": "truthy",
|
||||||
|
"actual": "result",
|
||||||
|
"message": "Should return true for valid email"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Actions (Act Phase)
|
||||||
|
|
||||||
|
- `function_call` - Call an imported function
|
||||||
|
- `render` - Render React component (requires React Testing Library)
|
||||||
|
- `click` - Simulate click event
|
||||||
|
- `fill` - Fill form input
|
||||||
|
- `select` - Select dropdown option
|
||||||
|
- `hover` - Hover over element
|
||||||
|
- `focus` - Focus on element
|
||||||
|
- `blur` - Blur from element
|
||||||
|
- `waitFor` - Wait for condition
|
||||||
|
|
||||||
|
### Supported Assertions
|
||||||
|
|
||||||
|
**Basic**: equals, deepEquals, notEquals, truthy, falsy
|
||||||
|
|
||||||
|
**Numeric**: greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual
|
||||||
|
|
||||||
|
**Type**: null, notNull, undefined, notUndefined, instanceOf
|
||||||
|
|
||||||
|
**Collection**: contains, matches, hasProperty, hasLength
|
||||||
|
|
||||||
|
**DOM**: toBeVisible, toBeInTheDocument, toHaveTextContent, toHaveAttribute, toHaveClass, toBeDisabled, toBeEnabled, toHaveValue
|
||||||
|
|
||||||
|
**Control**: throws, notThrows, custom
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Converter Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ConversionResult {
|
||||||
|
success: boolean;
|
||||||
|
jsonContent?: any; // The generated JSON
|
||||||
|
warnings: string[]; // Non-fatal issues
|
||||||
|
errors: string[]; // Fatal errors
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrator Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MigrationConfig {
|
||||||
|
dryRun?: boolean; // Preview only, don't write
|
||||||
|
verbose?: boolean; // Detailed logging
|
||||||
|
pattern?: string; // Glob pattern (default: 'frontends/**/*.test.ts')
|
||||||
|
targetDir?: string; // Output directory (default: 'packages')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Flags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry run (preview)
|
||||||
|
--dry-run
|
||||||
|
|
||||||
|
# Verbose logging
|
||||||
|
--verbose
|
||||||
|
|
||||||
|
# Custom glob pattern
|
||||||
|
--pattern 'src/**/*.test.ts'
|
||||||
|
|
||||||
|
# Custom target directory
|
||||||
|
--target-dir 'my-packages'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations & Fallbacks
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
|
||||||
|
1. **Complex Mocking** - Tests with advanced mock setup (spies, call tracking) may not convert perfectly
|
||||||
|
2. **Custom Hooks** - Tests with custom React hooks require manual adjustment
|
||||||
|
3. **Snapshots** - Snapshot tests require manual conversion
|
||||||
|
4. **Dynamic Imports** - Dynamic require() calls may not be captured
|
||||||
|
5. **Conditional Logic** - Complex conditional test logic may be simplified
|
||||||
|
|
||||||
|
### Handling Lossy Conversion
|
||||||
|
|
||||||
|
For tests that don't convert perfectly:
|
||||||
|
|
||||||
|
1. Run converter with `--verbose` to see warnings
|
||||||
|
2. Review warnings in output
|
||||||
|
3. Manually adjust the generated JSON as needed
|
||||||
|
4. Validate with validator tool
|
||||||
|
|
||||||
|
The 80/20 rule applies: ~80% of tests convert cleanly, ~20% need manual adjustment.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Recommended Workflow
|
||||||
|
|
||||||
|
1. **Backup** - Commit current state before migration
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "backup: before test migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Dry Run** - Preview what will happen
|
||||||
|
```bash
|
||||||
|
npx ts-node scripts/migrate-tests/migrator.ts --dry-run --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Migrate** - Run actual migration
|
||||||
|
```bash
|
||||||
|
npx ts-node scripts/migrate-tests/migrator.ts --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Validate** - Ensure JSON is valid
|
||||||
|
```bash
|
||||||
|
npx ts-node scripts/migrate-tests/validator.ts packages
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Test** - Run unified test runner
|
||||||
|
```bash
|
||||||
|
npm run test:unified
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Commit** - Save migration results
|
||||||
|
```bash
|
||||||
|
git add packages/*/unit-tests/
|
||||||
|
git commit -m "feat: migrate TypeScript tests to JSON format"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Simple Function Test
|
||||||
|
|
||||||
|
**TypeScript:**
|
||||||
|
```typescript
|
||||||
|
describe('Email Validation', () => {
|
||||||
|
it('should accept valid email', () => {
|
||||||
|
const result = validateEmail('user@example.com');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON (Converted):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"testSuites": [{
|
||||||
|
"name": "Email Validation",
|
||||||
|
"tests": [{
|
||||||
|
"name": "should accept valid email",
|
||||||
|
"act": {
|
||||||
|
"type": "function_call",
|
||||||
|
"target": "validateEmail",
|
||||||
|
"input": "user@example.com"
|
||||||
|
},
|
||||||
|
"assert": {
|
||||||
|
"expectations": [{
|
||||||
|
"type": "equals",
|
||||||
|
"expected": true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Test with Fixtures
|
||||||
|
|
||||||
|
**TypeScript:**
|
||||||
|
```typescript
|
||||||
|
it('should validate email from fixture', () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const result = validateEmail(email);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**JSON (Converted):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"arrange": {
|
||||||
|
"fixtures": { "email": "test@example.com" }
|
||||||
|
},
|
||||||
|
"act": {
|
||||||
|
"type": "function_call",
|
||||||
|
"target": "validateEmail",
|
||||||
|
"input": "$arrange.fixtures.email"
|
||||||
|
},
|
||||||
|
"assert": {
|
||||||
|
"expectations": [{
|
||||||
|
"type": "equals",
|
||||||
|
"expected": true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: "Schema not found"
|
||||||
|
**Solution**: Ensure `schemas/package-schemas/tests_schema.json` exists
|
||||||
|
|
||||||
|
### Issue: "No test files found"
|
||||||
|
**Solution**: Check glob pattern matches your test files
|
||||||
|
```bash
|
||||||
|
# Verify pattern:
|
||||||
|
npx ts-node scripts/migrate-tests/migrator.ts --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Package directory not created"
|
||||||
|
**Solution**: Ensure output directory exists and is writable
|
||||||
|
```bash
|
||||||
|
mkdir -p packages/my_package/unit-tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: "Validation errors after conversion"
|
||||||
|
**Solution**: Review warnings and adjust JSON manually as needed
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `converter.ts` - Main conversion logic (350+ lines)
|
||||||
|
- `migrator.ts` - Batch migration orchestration (250+ lines)
|
||||||
|
- `validator.ts` - JSON validation against schema (300+ lines)
|
||||||
|
- `index.ts` - Export module
|
||||||
|
- `README.md` - This file
|
||||||
|
|
||||||
|
## Integration with Unified Test Runner
|
||||||
|
|
||||||
|
After migration, tests are automatically discovered by the unified test runner:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UnifiedTestRunner } from '@/e2e/test-runner';
|
||||||
|
|
||||||
|
const runner = new UnifiedTestRunner();
|
||||||
|
const tests = await runner.discoverTests();
|
||||||
|
// Discovers: unit tests from packages/*/unit-tests/tests.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Run migration on existing TypeScript tests
|
||||||
|
2. Validate all converted JSON
|
||||||
|
3. Run unified test runner to execute tests
|
||||||
|
4. Document any manual adjustments needed
|
||||||
|
5. Update testing guidelines to use JSON format for new tests
|
||||||
386
scripts/migrate-tests/converter.ts
Normal file
386
scripts/migrate-tests/converter.ts
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/**
|
||||||
|
* TypeScript Test to JSON Test Converter
|
||||||
|
* Converts .test.ts files to declarative JSON format
|
||||||
|
*
|
||||||
|
* Strategy:
|
||||||
|
* - Parse describe() blocks → testSuites
|
||||||
|
* - Parse it() blocks → tests
|
||||||
|
* - Parse expect() statements → assertions
|
||||||
|
* - Extract fixture/mock setup → arrange phase
|
||||||
|
* - Convert test body to JSON representation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { dirname, basename } from 'path';
|
||||||
|
|
||||||
|
export interface ConversionResult {
|
||||||
|
success: boolean;
|
||||||
|
jsonContent?: any;
|
||||||
|
warnings: string[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestConverter {
|
||||||
|
private sourceFile!: ts.SourceFile;
|
||||||
|
private imports: Map<string, { from: string; items: string[] }> = new Map();
|
||||||
|
private warnings: string[] = [];
|
||||||
|
private errors: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a .test.ts file to JSON format
|
||||||
|
*/
|
||||||
|
convert(tsFilePath: string): ConversionResult {
|
||||||
|
this.imports.clear();
|
||||||
|
this.warnings = [];
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const source = readFileSync(tsFilePath, 'utf-8');
|
||||||
|
this.sourceFile = ts.createSourceFile(tsFilePath, source, ts.ScriptTarget.Latest, true);
|
||||||
|
|
||||||
|
// Extract imports
|
||||||
|
this.extractImports();
|
||||||
|
|
||||||
|
// Extract test suites
|
||||||
|
const testSuites = this.extractTestSuites();
|
||||||
|
|
||||||
|
if (testSuites.length === 0) {
|
||||||
|
this.warnings.push('No test suites found in file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine package name from file path
|
||||||
|
const packageName = this.extractPackageName(tsFilePath);
|
||||||
|
|
||||||
|
const jsonContent = {
|
||||||
|
$schema: 'https://metabuilder.dev/schemas/tests.schema.json',
|
||||||
|
schemaVersion: '2.0.0',
|
||||||
|
package: packageName,
|
||||||
|
description: `Converted from ${basename(tsFilePath)}`,
|
||||||
|
imports: Array.from(this.imports.values()),
|
||||||
|
testSuites: testSuites,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
jsonContent,
|
||||||
|
warnings: this.warnings,
|
||||||
|
errors: this.errors,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
this.errors.push(err instanceof Error ? err.message : String(err));
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
warnings: this.warnings,
|
||||||
|
errors: this.errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract imports from source file
|
||||||
|
*/
|
||||||
|
private extractImports(): void {
|
||||||
|
ts.forEachChild(this.sourceFile, (node) => {
|
||||||
|
if (ts.isImportDeclaration(node)) {
|
||||||
|
const from = (node.moduleSpecifier as any).text;
|
||||||
|
const imports: string[] = [];
|
||||||
|
|
||||||
|
if (node.importClause?.namedBindings) {
|
||||||
|
const bindings = node.importClause.namedBindings;
|
||||||
|
if (ts.isNamedImports(bindings)) {
|
||||||
|
for (const element of bindings.elements) {
|
||||||
|
imports.push(element.name.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imports.length > 0) {
|
||||||
|
this.imports.set(from, { from, items: imports });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract test suites (describe blocks) from source
|
||||||
|
*/
|
||||||
|
private extractTestSuites(): any[] {
|
||||||
|
const suites: any[] = [];
|
||||||
|
let suiteIndex = 0;
|
||||||
|
|
||||||
|
ts.forEachChild(this.sourceFile, (node) => {
|
||||||
|
if (this.isDescribeCall(node)) {
|
||||||
|
const suite = this.parseTestSuite(node, suiteIndex++);
|
||||||
|
if (suite) {
|
||||||
|
suites.push(suite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if node is a describe() call
|
||||||
|
*/
|
||||||
|
private isDescribeCall(node: ts.Node): node is ts.CallExpression {
|
||||||
|
return (
|
||||||
|
ts.isCallExpression(node) &&
|
||||||
|
ts.isIdentifier(node.expression) &&
|
||||||
|
node.expression.text === 'describe'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if node is an it() call
|
||||||
|
*/
|
||||||
|
private isItCall(node: ts.Node): node is ts.CallExpression {
|
||||||
|
return (
|
||||||
|
ts.isCallExpression(node) &&
|
||||||
|
ts.isIdentifier(node.expression) &&
|
||||||
|
node.expression.text === 'it'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse describe block into test suite
|
||||||
|
*/
|
||||||
|
private parseTestSuite(node: ts.CallExpression, index: number): any | null {
|
||||||
|
if (node.arguments.length < 2) return null;
|
||||||
|
|
||||||
|
const suiteName = this.extractStringLiteral(node.arguments[0]);
|
||||||
|
if (!suiteName) return null;
|
||||||
|
|
||||||
|
const tests: any[] = [];
|
||||||
|
let testIndex = 0;
|
||||||
|
|
||||||
|
if (node.arguments[1] && ts.isArrowFunction(node.arguments[1])) {
|
||||||
|
const body = node.arguments[1].body;
|
||||||
|
if (ts.isBlock(body)) {
|
||||||
|
ts.forEachChild(body, (child) => {
|
||||||
|
if (this.isItCall(child)) {
|
||||||
|
const test = this.parseTest(child, testIndex++);
|
||||||
|
if (test) {
|
||||||
|
tests.push(test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `suite_${index}`,
|
||||||
|
name: suiteName,
|
||||||
|
tests: tests,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse it block into test
|
||||||
|
*/
|
||||||
|
private parseTest(node: ts.CallExpression, index: number): any | null {
|
||||||
|
if (node.arguments.length < 2) return null;
|
||||||
|
|
||||||
|
const testName = this.extractStringLiteral(node.arguments[0]);
|
||||||
|
if (!testName) return null;
|
||||||
|
|
||||||
|
const test: any = {
|
||||||
|
id: `test_${index}`,
|
||||||
|
name: testName,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to extract arrange, act, assert from test body
|
||||||
|
if (node.arguments[1] && ts.isArrowFunction(node.arguments[1])) {
|
||||||
|
const body = node.arguments[1].body;
|
||||||
|
if (ts.isBlock(body)) {
|
||||||
|
this.extractTestPhases(body, test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract arrange, act, assert phases from test body
|
||||||
|
*/
|
||||||
|
private extractTestPhases(body: ts.Block, test: any): void {
|
||||||
|
// Simple heuristic: look for expect() calls
|
||||||
|
const expectations: any[] = [];
|
||||||
|
|
||||||
|
ts.forEachChild(body, (node) => {
|
||||||
|
if (this.isExpectCall(node)) {
|
||||||
|
const assertion = this.parseAssertion(node);
|
||||||
|
if (assertion) {
|
||||||
|
expectations.push(assertion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expectations.length > 0) {
|
||||||
|
test.assert = {
|
||||||
|
expectations: expectations,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no arrangements found, add minimal structure
|
||||||
|
if (!test.arrange) {
|
||||||
|
test.arrange = {
|
||||||
|
fixtures: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no act found, add minimal structure
|
||||||
|
if (!test.act) {
|
||||||
|
test.act = {
|
||||||
|
type: 'custom',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if node is an expect() call
|
||||||
|
*/
|
||||||
|
private isExpectCall(node: ts.Node): node is ts.CallExpression {
|
||||||
|
return (
|
||||||
|
ts.isCallExpression(node) &&
|
||||||
|
ts.isIdentifier(node.expression) &&
|
||||||
|
node.expression.text === 'expect'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse expect() call into assertion
|
||||||
|
*/
|
||||||
|
private parseAssertion(node: ts.CallExpression): any | null {
|
||||||
|
// expect(...).toBe(...) pattern
|
||||||
|
if (node.expression && ts.isPropertyAccessExpression(node.expression)) {
|
||||||
|
const matcher = node.expression.name.text;
|
||||||
|
const expected = node.arguments[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: this.mapMatcherToType(matcher),
|
||||||
|
expected: this.extractValue(expected),
|
||||||
|
description: `expect().${matcher}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map Vitest/Jest matchers to JSON assertion types
|
||||||
|
*/
|
||||||
|
private mapMatcherToType(matcher: string): string {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
toBe: 'equals',
|
||||||
|
toEqual: 'deepEquals',
|
||||||
|
toStrictEqual: 'strictEquals',
|
||||||
|
toNotEqual: 'notEquals',
|
||||||
|
toBeGreaterThan: 'greaterThan',
|
||||||
|
toBeLessThan: 'lessThan',
|
||||||
|
toBeGreaterThanOrEqual: 'greaterThanOrEqual',
|
||||||
|
toBeLessThanOrEqual: 'lessThanOrEqual',
|
||||||
|
toContain: 'contains',
|
||||||
|
toMatch: 'matches',
|
||||||
|
toThrow: 'throws',
|
||||||
|
toNotThrow: 'notThrows',
|
||||||
|
toBeTruthy: 'truthy',
|
||||||
|
toBeFalsy: 'falsy',
|
||||||
|
toBeNull: 'null',
|
||||||
|
toBeUndefined: 'undefined',
|
||||||
|
toBeDefined: 'notUndefined',
|
||||||
|
toBeInstanceOf: 'instanceOf',
|
||||||
|
toHaveProperty: 'hasProperty',
|
||||||
|
toHaveLength: 'hasLength',
|
||||||
|
toBeVisible: 'toBeVisible',
|
||||||
|
toBeInTheDocument: 'toBeInTheDocument',
|
||||||
|
toHaveTextContent: 'toHaveTextContent',
|
||||||
|
toHaveAttribute: 'toHaveAttribute',
|
||||||
|
toHaveClass: 'toHaveClass',
|
||||||
|
toBeDisabled: 'toBeDisabled',
|
||||||
|
toBeEnabled: 'toBeEnabled',
|
||||||
|
toHaveValue: 'toHaveValue',
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapping[matcher] || 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract string literal from node
|
||||||
|
*/
|
||||||
|
private extractStringLiteral(node: ts.Expression | undefined): string | null {
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract value from node
|
||||||
|
*/
|
||||||
|
private extractValue(node: ts.Expression | undefined): any {
|
||||||
|
if (!node) return undefined;
|
||||||
|
|
||||||
|
if (ts.isStringLiteral(node)) {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts.isNumericLiteral(node)) {
|
||||||
|
return Number(node.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.kind === ts.SyntaxKind.NullKeyword) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract package name from file path
|
||||||
|
*/
|
||||||
|
private extractPackageName(filePath: string): string {
|
||||||
|
const match = filePath.match(/packages\/([^/]+)\//);
|
||||||
|
return match ? match[1] : 'unknown_package';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a single file
|
||||||
|
*/
|
||||||
|
export function convertFile(tsPath: string, jsonPath: string): boolean {
|
||||||
|
const converter = new TestConverter();
|
||||||
|
const result = converter.convert(tsPath);
|
||||||
|
|
||||||
|
if (!result.success || !result.jsonContent) {
|
||||||
|
console.error(`✗ Failed to convert ${tsPath}`);
|
||||||
|
result.errors.forEach(err => console.error(` Error: ${err}`));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(jsonPath, JSON.stringify(result.jsonContent, null, 2));
|
||||||
|
console.log(`✓ Converted ${tsPath} → ${jsonPath}`);
|
||||||
|
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
result.warnings.forEach(warn => console.warn(` ⚠️ ${warn}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ Failed to write ${jsonPath}:`, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
scripts/migrate-tests/index.ts
Normal file
12
scripts/migrate-tests/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Test Migration Toolkit
|
||||||
|
* Exports for batch test migration operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { TestConverter, ConversionResult } from './converter';
|
||||||
|
export { TestMigrator, MigrationConfig } from './migrator';
|
||||||
|
export { TestValidator, ValidationResult } from './validator';
|
||||||
|
|
||||||
|
export { convertFile } from './converter';
|
||||||
|
export { runMigration } from './migrator';
|
||||||
|
export { validateTests } from './validator';
|
||||||
175
scripts/migrate-tests/migrator.ts
Normal file
175
scripts/migrate-tests/migrator.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Batch Test Migration Script
|
||||||
|
* Discovers all .test.ts files and converts to JSON
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { glob } from 'glob';
|
||||||
|
import { mkdirSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { convertFile } from './converter';
|
||||||
|
|
||||||
|
export interface MigrationConfig {
|
||||||
|
dryRun?: boolean;
|
||||||
|
verbose?: boolean;
|
||||||
|
pattern?: string;
|
||||||
|
targetDir?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestMigrator {
|
||||||
|
private config: MigrationConfig;
|
||||||
|
private converted: number = 0;
|
||||||
|
private failed: number = 0;
|
||||||
|
private skipped: number = 0;
|
||||||
|
|
||||||
|
constructor(config: MigrationConfig = {}) {
|
||||||
|
this.config = {
|
||||||
|
dryRun: false,
|
||||||
|
verbose: false,
|
||||||
|
pattern: 'frontends/**/*.test.ts',
|
||||||
|
targetDir: 'packages',
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run migration
|
||||||
|
*/
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
console.log('╔════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ TypeScript → JSON Test Migration Tool ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
if (this.config.dryRun) {
|
||||||
|
console.log('⚠️ DRY RUN MODE - No files will be modified\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover test files
|
||||||
|
const testFiles = await this.discoverTestFiles();
|
||||||
|
|
||||||
|
if (testFiles.length === 0) {
|
||||||
|
console.log('No test files found matching pattern: ' + this.config.pattern);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${testFiles.length} test files\n`);
|
||||||
|
|
||||||
|
// Convert each file
|
||||||
|
for (const testFile of testFiles) {
|
||||||
|
await this.convertTestFile(testFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ Migration Summary ║');
|
||||||
|
console.log('╠════════════════════════════════════════════════════════╣');
|
||||||
|
console.log(`║ ✓ Converted: ${String(this.converted).padEnd(38)}║`);
|
||||||
|
console.log(`║ ✗ Failed: ${String(this.failed).padEnd(38)}║`);
|
||||||
|
console.log(`║ ⊘ Skipped: ${String(this.skipped).padEnd(38)}║`);
|
||||||
|
console.log('╚════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
if (this.config.dryRun) {
|
||||||
|
console.log('This was a DRY RUN. Files were not actually modified.');
|
||||||
|
console.log('Run without --dry-run to perform actual migration.\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover test files
|
||||||
|
*/
|
||||||
|
private async discoverTestFiles(): Promise<string[]> {
|
||||||
|
return await glob(this.config.pattern!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a single test file
|
||||||
|
*/
|
||||||
|
private async convertTestFile(testFile: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Determine output path
|
||||||
|
const packageName = this.extractPackageName(testFile);
|
||||||
|
const jsonPath = join(this.config.targetDir!, packageName, 'unit-tests', 'tests.json');
|
||||||
|
|
||||||
|
if (this.config.verbose) {
|
||||||
|
console.log(`Processing: ${testFile}`);
|
||||||
|
console.log(`Target: ${jsonPath}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.dryRun) {
|
||||||
|
console.log(`[DRY RUN] Would convert: ${testFile}`);
|
||||||
|
console.log(`[DRY RUN] → ${jsonPath}\n`);
|
||||||
|
this.converted++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
mkdirSync(dirname(jsonPath), { recursive: true });
|
||||||
|
|
||||||
|
// Convert file
|
||||||
|
if (convertFile(testFile, jsonPath)) {
|
||||||
|
this.converted++;
|
||||||
|
} else {
|
||||||
|
this.failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.verbose) {
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ Error processing ${testFile}:`, err);
|
||||||
|
this.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract package name from test file path
|
||||||
|
*/
|
||||||
|
private extractPackageName(testFile: string): string {
|
||||||
|
// Handle different patterns
|
||||||
|
if (testFile.includes('frontends/nextjs')) {
|
||||||
|
return 'nextjs_frontend';
|
||||||
|
}
|
||||||
|
if (testFile.includes('frontends/cli')) {
|
||||||
|
return 'cli_frontend';
|
||||||
|
}
|
||||||
|
if (testFile.includes('frontends/qt6')) {
|
||||||
|
return 'qt6_frontend';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: extract from path
|
||||||
|
const match = testFile.match(/frontends\/([^/]+)\//);
|
||||||
|
return match ? `${match[1]}_tests` : 'unknown_tests';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get migration statistics
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
converted: this.converted,
|
||||||
|
failed: this.failed,
|
||||||
|
skipped: this.skipped,
|
||||||
|
total: this.converted + this.failed + this.skipped,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
export async function runMigration(config?: MigrationConfig): Promise<void> {
|
||||||
|
const migrator = new TestMigrator(config);
|
||||||
|
await migrator.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI support
|
||||||
|
if (require.main === module) {
|
||||||
|
const config: MigrationConfig = {
|
||||||
|
dryRun: process.argv.includes('--dry-run'),
|
||||||
|
verbose: process.argv.includes('--verbose'),
|
||||||
|
};
|
||||||
|
|
||||||
|
runMigration(config).catch(err => {
|
||||||
|
console.error('\n❌ Migration failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
232
scripts/migrate-tests/validator.ts
Normal file
232
scripts/migrate-tests/validator.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
/**
|
||||||
|
* Test Validator
|
||||||
|
* Validates converted JSON test files against tests_schema.json
|
||||||
|
* Can be used pre- or post-migration to ensure quality
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import Ajv from 'ajv';
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
file: string;
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestValidator {
|
||||||
|
private ajv: Ajv;
|
||||||
|
private schema: any;
|
||||||
|
|
||||||
|
constructor(schemaPath: string) {
|
||||||
|
this.ajv = new Ajv({ allErrors: true });
|
||||||
|
try {
|
||||||
|
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
||||||
|
this.schema = JSON.parse(schemaContent);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to load schema from ${schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a single JSON test file
|
||||||
|
*/
|
||||||
|
validate(jsonPath: string): ValidationResult {
|
||||||
|
const result: ValidationResult = {
|
||||||
|
file: jsonPath,
|
||||||
|
valid: false,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(jsonPath, 'utf-8');
|
||||||
|
const jsonContent = JSON.parse(content);
|
||||||
|
|
||||||
|
// Validate against schema
|
||||||
|
const validate = this.ajv.compile(this.schema);
|
||||||
|
const isValid = validate(jsonContent);
|
||||||
|
|
||||||
|
if (!isValid && validate.errors) {
|
||||||
|
result.errors = validate.errors.map(err => {
|
||||||
|
const path = err.instancePath || 'root';
|
||||||
|
return `${path}: ${err.message}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional validations
|
||||||
|
this.performAdditionalChecks(jsonContent, result);
|
||||||
|
|
||||||
|
result.valid = result.errors.length === 0;
|
||||||
|
} catch (err) {
|
||||||
|
result.errors.push(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all JSON test files in a directory
|
||||||
|
*/
|
||||||
|
validateDirectory(dirPath: string): ValidationResult[] {
|
||||||
|
const results: ValidationResult[] = [];
|
||||||
|
const files = this.findJsonFiles(dirPath);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
results.push(this.validate(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all JSON files in directory
|
||||||
|
*/
|
||||||
|
private findJsonFiles(dirPath: string): string[] {
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...this.findJsonFiles(fullPath));
|
||||||
|
} else if (entry.name.endsWith('.json') && entry.name !== 'metadata.json') {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to read directory ${dirPath}:`, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform additional semantic checks beyond schema validation
|
||||||
|
*/
|
||||||
|
private performAdditionalChecks(content: any, result: ValidationResult): void {
|
||||||
|
// Check that test IDs are unique
|
||||||
|
const testIds = new Set<string>();
|
||||||
|
const suiteIds = new Set<string>();
|
||||||
|
|
||||||
|
if (content.testSuites) {
|
||||||
|
for (const suite of content.testSuites) {
|
||||||
|
if (suiteIds.has(suite.id)) {
|
||||||
|
result.warnings.push(`Duplicate suite ID: ${suite.id}`);
|
||||||
|
}
|
||||||
|
suiteIds.add(suite.id);
|
||||||
|
|
||||||
|
if (suite.tests) {
|
||||||
|
for (const test of suite.tests) {
|
||||||
|
if (testIds.has(test.id)) {
|
||||||
|
result.warnings.push(`Duplicate test ID: ${test.id}`);
|
||||||
|
}
|
||||||
|
testIds.add(test.id);
|
||||||
|
|
||||||
|
// Check assertions exist
|
||||||
|
if (!test.assert?.expectations || test.assert.expectations.length === 0) {
|
||||||
|
result.warnings.push(`Test "${test.name}" has no assertions`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check imports are referenced
|
||||||
|
const imports = new Set<string>();
|
||||||
|
if (content.imports) {
|
||||||
|
for (const imp of content.imports) {
|
||||||
|
if (imp.items) {
|
||||||
|
for (const item of imp.items) {
|
||||||
|
imports.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn about unused imports (basic check)
|
||||||
|
const contentStr = JSON.stringify(content);
|
||||||
|
for (const imp of imports) {
|
||||||
|
if (!contentStr.includes(`"${imp}"`) && !contentStr.includes(`${imp}:`)) {
|
||||||
|
result.warnings.push(`Import "${imp}" appears unused`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point
|
||||||
|
*/
|
||||||
|
export async function validateTests(
|
||||||
|
testPath: string,
|
||||||
|
schemaPath: string = 'schemas/package-schemas/tests_schema.json'
|
||||||
|
): Promise<void> {
|
||||||
|
console.log('╔════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ Test JSON Validator ║');
|
||||||
|
console.log('╚════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validator = new TestValidator(schemaPath);
|
||||||
|
|
||||||
|
let results: ValidationResult[];
|
||||||
|
|
||||||
|
const stats = fs.statSync(testPath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
console.log(`Validating all JSON tests in: ${testPath}\n`);
|
||||||
|
results = validator.validateDirectory(testPath);
|
||||||
|
} else {
|
||||||
|
console.log(`Validating: ${testPath}\n`);
|
||||||
|
results = [validator.validate(testPath)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
let validCount = 0;
|
||||||
|
let invalidCount = 0;
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const status = result.valid ? '✓' : '✗';
|
||||||
|
console.log(`${status} ${result.file}`);
|
||||||
|
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
result.errors.forEach(err => console.error(` Error: ${err}`));
|
||||||
|
invalidCount++;
|
||||||
|
} else {
|
||||||
|
validCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
result.warnings.forEach(warn => console.warn(` ⚠️ ${warn}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('\n╔════════════════════════════════════════════════════════╗');
|
||||||
|
console.log('║ Validation Summary ║');
|
||||||
|
console.log('╠════════════════════════════════════════════════════════╣');
|
||||||
|
console.log(`║ ✓ Valid: ${String(validCount).padEnd(40)}║`);
|
||||||
|
console.log(`║ ✗ Invalid: ${String(invalidCount).padEnd(40)}║`);
|
||||||
|
console.log('╚════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
if (invalidCount > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Validation failed:', err instanceof Error ? err.message : String(err));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI support
|
||||||
|
if (require.main === module) {
|
||||||
|
const testPath = process.argv[2] || 'packages';
|
||||||
|
const schemaPath = process.argv[3] || 'schemas/package-schemas/tests_schema.json';
|
||||||
|
|
||||||
|
validateTests(testPath, schemaPath).catch(err => {
|
||||||
|
console.error('Validation error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user