UNPKG

nerdamer-prime

Version:
672 lines (565 loc) 31 kB
// Load complete nerdamer with all modules import nerdamerRuntime from '../all'; import tsMorph from 'ts-morph'; // Type alias for dynamic property access in reflection tests type Indexable = Record<string, any>; const nerdamer = nerdamerRuntime as typeof nerdamerRuntime & Indexable; describe('Nerdamer TypeScript Interface Reflection', () => { let project: tsMorph.Project; let sourceFile: tsMorph.SourceFile; // eslint-disable-next-line @typescript-eslint/no-unused-vars let typeChecker: tsMorph.TypeChecker; /** * Helper to get method-like members from an interface. The index.d.ts uses property signatures with function types * (e.g., `add: (x: T) => R`) instead of method signatures (e.g., `add(x: T): R`), so we need to check properties * whose type is a function type. */ const getMethodLikeMembers = (iface: tsMorph.InterfaceDeclaration | undefined): string[] => { if (!iface) { return []; } const methodNames: string[] = []; // Check actual method signatures iface.getMethods().forEach(m => { methodNames.push(m.getName()); }); // Check property signatures with function types iface.getProperties().forEach(p => { const typeNode = p.getTypeNode(); if (typeNode?.getKind() === tsMorph.SyntaxKind.FunctionType) { methodNames.push(p.getName()); } }); return methodNames; }; /** Helper to get a method-like member by name (either method signature or property with function type) */ const getMethodLikeMember = ( iface: tsMorph.InterfaceDeclaration | undefined, name: string ): | { getReturnTypeNode: () => tsMorph.TypeNode | undefined; getParameters: () => { getTypeNode: () => tsMorph.TypeNode | undefined }[]; } | undefined => { if (!iface) { return undefined; } // Try method signature first const method = iface.getMethod(name); if (method) { return { getReturnTypeNode: () => method.getReturnTypeNode(), getParameters: () => method.getParameters(), }; } // Try property with function type const prop = iface.getProperty(name); if (prop) { const typeNode = prop.getTypeNode(); if (typeNode?.getKind() === tsMorph.SyntaxKind.FunctionType) { const funcType = typeNode as tsMorph.FunctionTypeNode; return { getReturnTypeNode: () => funcType.getReturnTypeNode(), getParameters: () => funcType.getParameters().map(p => ({ getTypeNode: () => p.getTypeNode(), })), }; } } return undefined; }; beforeAll(() => { project = new tsMorph.Project({ tsConfigFilePath: 'tsconfig.json', }); sourceFile = project.getSourceFileOrThrow('index.d.ts'); typeChecker = project.getTypeChecker(); }); describe('Core Type Aliases', () => { const expectedTypeAliases = [ 'OutputType', 'ParseOption', 'ExpressionHistoryIndex', 'int', 'ExpressionParam', 'SolveResult', ]; it('should have all expected type aliases defined', () => { const typeAliases = sourceFile.getTypeAliases().map((ta: tsMorph.TypeAliasDeclaration) => ta.getName()); for (const expectedAlias of expectedTypeAliases) { expect(typeAliases).toContain(expectedAlias); } }); it('should have OutputType with correct union members', () => { const outputType = sourceFile.getTypeAlias('OutputType'); expect(outputType).toBeDefined(); const typeNode = outputType?.getTypeNode(); if (typeNode?.getKind() === tsMorph.SyntaxKind.UnionType) { const unionType = typeNode.asKindOrThrow(tsMorph.SyntaxKind.UnionType); const members = unionType .getTypeNodes() .map((tn: tsMorph.TypeNode) => { const text = tn.getText(); // Remove quotes from string literals return text.startsWith("'") && text.endsWith("'") ? text.slice(1, -1) : text; }) .filter(Boolean); expect(members).toContain('decimals'); expect(members).toContain('fractions'); expect(members).toContain('scientific'); expect(members).toContain('mixed'); expect(members).toContain('recurring'); } }); it('should have ExpressionParam with correct union structure', () => { const expressionParam = sourceFile.getTypeAlias('ExpressionParam'); expect(expressionParam).toBeDefined(); const typeText = expressionParam?.getTypeNode()?.getText(); expect(typeText).toContain('string'); expect(typeText).toContain('number'); expect(typeText).toContain('NerdamerExpression'); expect(typeText).toContain('NerdamerEquation'); }); }); describe('Core Expression Interfaces', () => { it('should have NerdamerExpression interface with essential output methods', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); expect(nerdamerExpression).toBeDefined(); const methodNames = getMethodLikeMembers(nerdamerExpression); // These methods were previously in CoreExpressionBase but are now directly on NerdamerExpression const expectedMethods = ['toString', 'text', 'latex', 'valueOf']; for (const method of expectedMethods) { expect(methodNames).toContain(method); } }); it('should have NerdamerExpression interface as standalone (not extending CoreExpressionBase)', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); expect(nerdamerExpression).toBeDefined(); const extendsClause = nerdamerExpression?.getExtends(); // NerdamerExpression no longer extends CoreExpressionBase - methods are directly defined expect(extendsClause).toHaveLength(0); }); it('should have NerdamerExpression with essential arithmetic methods', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const methodNames = getMethodLikeMembers(nerdamerExpression); const arithmeticMethods = ['add', 'subtract', 'multiply', 'divide', 'pow']; for (const method of arithmeticMethods) { expect(methodNames).toContain(method); } }); it('should have NerdamerExpression with comparison methods', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const methodNames = getMethodLikeMembers(nerdamerExpression); const comparisonMethods = ['eq', 'lt', 'gt', 'lte', 'gte']; for (const method of comparisonMethods) { expect(methodNames).toContain(method); } }); it('should have NerdamerExpression with manipulation methods', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const methodNames = getMethodLikeMembers(nerdamerExpression); // Note: 'factor' is a static method, not an instance method const manipulationMethods = ['expand', 'simplify', 'evaluate', 'sub', 'solveFor']; for (const method of manipulationMethods) { expect(methodNames).toContain(method); } }); }); describe('JavaScript Runtime vs TypeScript Interface Comparison', () => { it('should validate NerdamerExpression methods exist on runtime instances', () => { // Create a runtime expression const runtimeExpr = nerdamerRuntime('x + 1') as Indexable; // Get TypeScript interface methods (including property signatures with function types) const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const tsMethodNames = getMethodLikeMembers(nerdamerExpression); // Test that each TS method exists on the runtime object const methodTestResults = []; for (const methodName of tsMethodNames) { const exists = typeof runtimeExpr[methodName] === 'function'; const actualType = typeof runtimeExpr[methodName]; methodTestResults.push({ method: methodName, exists, type: actualType }); } const missingMethods = methodTestResults.filter(r => !r.exists); if (missingMethods.length > 0) { console.log( 'Missing methods in runtime:', missingMethods.map(m => m.method) ); } // Allow some methods to be missing since some might be inherited or added dynamically expect(missingMethods.length).toBeLessThan(tsMethodNames.length * 0.3); // Allow up to 30% missing }); it('should validate runtime expression behavior matches TypeScript expectations', () => { const runtimeExpr = nerdamerRuntime('x^2 + 2*x + 1'); // Test basic operations that should exist per TypeScript expect(typeof runtimeExpr.toString).toBe('function'); expect(typeof runtimeExpr.text).toBe('function'); expect(typeof runtimeExpr.evaluate).toBe('function'); expect(typeof runtimeExpr.expand).toBe('function'); expect(typeof runtimeExpr.simplify).toBe('function'); // Test that operations return expected types const expanded = runtimeExpr.expand(); expect(expanded).toBeDefined(); expect(typeof expanded.toString).toBe('function'); // Test factor as static method (not instance method) expect(typeof nerdamerRuntime.factor).toBe('function'); const factored = nerdamerRuntime.factor(runtimeExpr); expect(factored).toBeDefined(); expect(typeof factored.toString).toBe('function'); }); it('should validate arithmetic operations work as TypeScript suggests', () => { const expr1 = nerdamerRuntime('x + 1'); const expr2 = nerdamerRuntime('y + 2'); // Test that arithmetic methods exist and work if (typeof expr1.add === 'function') { const sum = expr1.add(expr2); expect(sum).toBeDefined(); expect(typeof sum.toString).toBe('function'); } if (typeof expr1.multiply === 'function') { const product = expr1.multiply(2); expect(product).toBeDefined(); expect(typeof product.toString).toBe('function'); } if (typeof expr1.pow === 'function') { const power = expr1.pow(2); expect(power).toBeDefined(); expect(typeof power.toString).toBe('function'); } }); it('should validate comparison operations work as TypeScript suggests', () => { const expr1 = nerdamerRuntime('5'); // eslint-disable-next-line @typescript-eslint/no-unused-vars const expr2 = nerdamerRuntime('3'); // Test comparison methods if they exist if (typeof expr1.eq === 'function') { const isEqual = expr1.eq(5); expect(typeof isEqual).toBe('boolean'); } if (typeof expr1.gt === 'function') { const isGreater = expr1.gt(3); expect(typeof isGreater).toBe('boolean'); } if (typeof expr1.lt === 'function') { const isLess = expr1.lt(10); expect(typeof isLess).toBe('boolean'); } }); it('should validate buildFunction method works as TypeScript suggests', () => { const expr = nerdamerRuntime('x^2 + y'); if (typeof expr.buildFunction === 'function') { try { const fn = expr.buildFunction(['x', 'y']); expect(typeof fn).toBe('function'); // Test that the generated function works const result = fn(2, 3); expect(typeof result).toBe('number'); expect(result).toBe(7); // 2^2 + 3 = 7 } catch (error) { // If buildFunction fails, just log it but don't fail the test console.log('buildFunction test failed:', (error as Error).message); } } }); it('should validate equation handling matches TypeScript definitions', () => { const equation = nerdamerRuntime('x^2 = 4') as Indexable; // Check if it's recognized as an equation (has LHS and RHS properties) if (equation['LHS'] && equation['RHS']) { expect(typeof equation['LHS'].toString).toBe('function'); expect(typeof equation['RHS'].toString).toBe('function'); if (typeof equation['toLHS'] === 'function') { const lhsForm = equation['toLHS'](); expect(lhsForm).toBeDefined(); expect(typeof lhsForm.toString).toBe('function'); } } // Test solve functionality if available if (typeof equation['solveFor'] === 'function') { try { const solutions = equation['solveFor']('x'); expect(Array.isArray(solutions)).toBe(true); } catch (error) { // Some equations might not be solvable, that's ok console.log('Equation solving test failed (expected for some cases):', (error as Error).message); } } }); }); describe('NerdamerCore Namespace Interfaces', () => { let nerdamerCoreNamespace: tsMorph.ModuleDeclaration | undefined; beforeAll(() => { const primeNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamerPrime'); nerdamerCoreNamespace = primeNamespace?.getModules().find(m => m.getName() === 'NerdamerCore'); expect(nerdamerCoreNamespace).toBeDefined(); }); it('should have core data structure interfaces', () => { // These are the main data structure interfaces (not constructors) const expectedInterfaces = ['Frac', 'NerdamerSymbol', 'Vector', 'Matrix', 'NerdamerSet', 'Collection']; for (const ifaceName of expectedInterfaces) { const iface = nerdamerCoreNamespace?.getInterface(ifaceName); expect(iface).toBeDefined(); } }); it('should have Core interface with essential properties', () => { const coreInterface = nerdamerCoreNamespace?.getInterface('Core'); expect(coreInterface).toBeDefined(); // Core should have essential types like PARSER, NerdamerSymbol constructor, etc. const memberNames = coreInterface?.getMembers().map(m => { // TypeElementTypes can be PropertySignature, MethodSignature, etc. // Check if it has getName method (most do) if ('getName' in m && typeof m.getName === 'function') { return m.getName(); } return undefined; }) || []; expect(memberNames.length).toBeGreaterThan(0); }); it('should validate Vector/Matrix are properly wrapped Expressions', () => { // Test vector creation and base expression functionality if (typeof nerdamerRuntime.vector === 'function') { try { const vec = nerdamerRuntime.vector([1, 2, 3]) as Indexable; expect(vec).toBeDefined(); // Test that vector IS an expression (has base expression methods) const baseExpressionMethods = [ 'toString', 'text', 'evaluate', 'add', 'multiply', 'expand', // Note: 'clone' removed due to Expression prototype inconsistency ]; const existingBaseMethods = baseExpressionMethods.filter( (method: string) => typeof vec[method] === 'function' ); // Vector should have most base expression methods since it extends NerdamerExpression expect(existingBaseMethods.length).toBeGreaterThan(4); // Test vector-specific properties or behavior (if any) // Vectors use static functions like nerdamer.vecget(), not instance methods expect(typeof nerdamerRuntime.vecget).toBe('function'); expect(typeof nerdamerRuntime.dot).toBe('function'); expect(typeof nerdamerRuntime.cross).toBe('function'); } catch (error) { console.log('Vector test failed:', (error as Error).message); } } // Test matrix creation and base expression functionality if (typeof nerdamerRuntime.matrix === 'function') { try { const mat = nerdamerRuntime.matrix([ [1, 2], [3, 4], ]) as Indexable; expect(mat).toBeDefined(); // Test that matrix IS an expression (has base expression methods) const baseExpressionMethods = [ 'toString', 'text', 'evaluate', 'add', 'multiply', 'expand', // Note: 'clone' removed due to Expression prototype inconsistency ]; const existingBaseMethods = baseExpressionMethods.filter( (method: string) => typeof mat[method] === 'function' ); // Matrix should have most base expression methods since it extends NerdamerExpression expect(existingBaseMethods.length).toBeGreaterThan(4); // Test matrix-specific properties or behavior (if any) // Matrices use static functions like nerdamer.matget(), not instance methods expect(typeof nerdamerRuntime.matget).toBe('function'); expect(typeof nerdamerRuntime.determinant).toBe('function'); expect(typeof nerdamerRuntime.transpose).toBe('function'); } catch (error) { console.log('Matrix test failed:', (error as Error).message); } } }); }); describe('Main Function Interface vs Runtime', () => { it('should have overloaded nerdamer function declarations matching runtime behavior', () => { const functionDeclarations = sourceFile.getFunctions(); expect(functionDeclarations.length).toBeGreaterThan(0); // Test that runtime nerdamer function works with different parameter types expect(typeof nerdamerRuntime).toBe('function'); // Test string input const expr1 = nerdamerRuntime('x + 1'); expect(expr1).toBeDefined(); // Test with substitutions const expr2 = nerdamerRuntime('x + 1', { x: 2 }); expect(expr2).toBeDefined(); // Test with options const expr3 = nerdamerRuntime('x^2 + 2*x + 1', {}, 'expand'); expect(expr3).toBeDefined(); }); it('should validate static methods exist on runtime nerdamer function', () => { // Get all static methods from TypeScript namespace const nerdamerNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamer'); // Get export declarations and manually filter to find import equals declarations const exportDeclarations = nerdamerNamespace?.getExportedDeclarations(); const importDeclarations: tsMorph.ImportEqualsDeclaration[] = []; if (exportDeclarations) { for (const [_key, declarations] of exportDeclarations) { for (const decl of declarations) { if (decl.getKind?.() === tsMorph.SyntaxKind.ImportEqualsDeclaration) { importDeclarations.push(decl as unknown as tsMorph.ImportEqualsDeclaration); } } } } // Extract method names from import declarations const staticMethods = importDeclarations.map((decl: tsMorph.ImportEqualsDeclaration) => decl.getName()); // Test that at least some of these exist on the runtime const existingStaticMethods = staticMethods.filter( (method: string) => typeof nerdamer[method] !== 'undefined' ); console.log( `Static methods in TS: ${staticMethods.length}, Existing in runtime: ${existingStaticMethods.length}` ); // Should have a good percentage of the TypeScript static methods expect(existingStaticMethods.length).toBeGreaterThan(staticMethods.length * 0.5); // Test some essential ones specifically const essentialMethods = ['expand', 'factor', 'simplify', 'solve', 'diff', 'integrate', 'cos', 'sin']; for (const method of essentialMethods) { if (staticMethods.includes(method)) { expect(nerdamer[method]).toBeDefined(); expect(typeof nerdamer[method]).toBe('function'); } } }); }); describe('Interface Consistency', () => { it('should have consistent return types for arithmetic operations', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const arithmeticMethods = ['add', 'subtract', 'multiply', 'divide', 'pow']; for (const methodName of arithmeticMethods) { const method = getMethodLikeMember(nerdamerExpression, methodName); const returnType = method?.getReturnTypeNode()?.getText(); expect(returnType).toBe('NerdamerExpression'); } }); it('should have consistent parameter types for arithmetic operations', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const arithmeticMethods = ['add', 'subtract', 'multiply', 'divide', 'pow']; for (const methodName of arithmeticMethods) { const method = getMethodLikeMember(nerdamerExpression, methodName); const parameters = method?.getParameters(); expect(parameters?.length).toBe(1); const paramType = parameters?.[0]?.getTypeNode()?.getText(); expect(paramType).toBe('ExpressionParam'); } }); it('should have consistent boolean return types for comparison operations', () => { const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const comparisonMethods = ['eq', 'lt', 'gt', 'lte', 'gte']; for (const methodName of comparisonMethods) { const method = getMethodLikeMember(nerdamerExpression, methodName); const returnType = method?.getReturnTypeNode()?.getText(); expect(returnType).toBe('boolean'); } }); }); describe('Runtime to TypeScript Coverage Analysis', () => { it('should analyze runtime expression prototype vs TypeScript interface coverage', () => { const expr = nerdamerRuntime('x + 1'); // Get all methods including inherited ones by walking the prototype chain const getAllMethods = (obj: unknown): string[] => { const methods = new Set<string>(); let current = obj; while (current && current !== Object.prototype) { Object.getOwnPropertyNames(current).forEach(name => { if (typeof (obj as Record<string, unknown>)[name] === 'function') { methods.add(name); } }); current = Object.getPrototypeOf(current); } return Array.from(methods).sort(); }; const runtimeMethods = getAllMethods(expr); // Get TypeScript methods from both NerdamerExpression and CoreExpressionBase const nerdamerExpression = sourceFile.getInterface('NerdamerExpression'); const coreExpressionBase = sourceFile.getInterface('CoreExpressionBase'); const nerdamerMethods = getMethodLikeMembers(nerdamerExpression); const coreMethods = getMethodLikeMembers(coreExpressionBase); const allTsMethods = [...new Set([...nerdamerMethods, ...coreMethods])].sort(); const missingInTs = runtimeMethods.filter(method => !allTsMethods.includes(method)); const missingInRuntime = allTsMethods.filter((method: string) => !runtimeMethods.includes(method)); console.log('\n=== Expression Method Coverage Analysis ==='); console.log(`Runtime methods: ${runtimeMethods.length}`); console.log(`TypeScript methods: ${allTsMethods.length}`); console.log(`Missing in TypeScript (first 10):`, missingInTs.slice(0, 10)); console.log(`Missing in Runtime (first 10):`, missingInRuntime.slice(0, 10)); // Calculate coverage const coverage = (allTsMethods.length - missingInRuntime.length) / allTsMethods.length; console.log(`TypeScript to Runtime coverage: ${Math.round(coverage * 100)}%`); // Expect high coverage since we now include inherited methods expect(coverage).toBeGreaterThan(0.8); // At least 80% coverage }); it('should analyze static function coverage', () => { const runtimeStaticMethods = Object.getOwnPropertyNames(nerdamerRuntime) .filter((name: string) => typeof nerdamer[name] === 'function') .sort(); const nerdamerNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamer'); // Get export declarations and manually filter to find import equals declarations const exportDeclarations = nerdamerNamespace?.getExportedDeclarations(); const importEqualsDeclarations: tsMorph.ImportEqualsDeclaration[] = []; if (exportDeclarations) { for (const [_key, declarations] of exportDeclarations) { for (const decl of declarations) { if (decl.getKind?.() === tsMorph.SyntaxKind.ImportEqualsDeclaration) { importEqualsDeclarations.push(decl as unknown as tsMorph.ImportEqualsDeclaration); } } } } const tsStaticMethods = importEqualsDeclarations .map((decl: tsMorph.ImportEqualsDeclaration) => decl.getName()) // Filter out TypeScript-only constructs like 'NerdamerCore' .filter((name: string) => name !== 'NerdamerCore') .sort(); const missingInTs = runtimeStaticMethods.filter(method => !tsStaticMethods.includes(method)); const missingInRuntime = tsStaticMethods.filter((method: string) => !runtimeStaticMethods.includes(method)); console.log('\n=== Static Method Coverage Analysis ==='); console.log(`Runtime static methods: ${runtimeStaticMethods.length}`); console.log(`TypeScript static methods: ${tsStaticMethods.length}`); console.log(`Missing in TypeScript (first 10):`, missingInTs.slice(0, 10)); console.log(`Missing in Runtime (first 10):`, missingInRuntime.slice(0, 10)); // Calculate coverage const coverage = (tsStaticMethods.length - missingInRuntime.length) / tsStaticMethods.length; console.log(`TypeScript to Runtime static coverage: ${Math.round(coverage * 100)}%`); // Expect very high coverage since we exclude TypeScript-only constructs expect(coverage).toBeGreaterThan(0.95); // At least 95% coverage for static methods }); }); describe('Completeness Check', () => { it('should have a reasonable number of interfaces defined', () => { const allInterfaces = sourceFile.getInterfaces(); const primeNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamerPrime'); const nerdamerCoreInterfaces = primeNamespace ?.getModules() .find(m => m.getName() === 'NerdamerCore') ?.getInterfaces() || []; const totalInterfaces = allInterfaces.length + nerdamerCoreInterfaces.length; // Should have a substantial number of interfaces for a comprehensive math library expect(totalInterfaces).toBeGreaterThan(20); }); it('should have proper namespace organization', () => { // Main namespace should exist const nerdamerNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamer'); expect(nerdamerNamespace).toBeDefined(); // NerdamerPrime namespace should exist const primeNamespace = sourceFile.getModules().find(m => m.getName() === 'nerdamerPrime'); expect(primeNamespace).toBeDefined(); // NerdamerCore namespace should exist inside nerdamerPrime const coreNamespace = primeNamespace?.getModules().find(m => m.getName() === 'NerdamerCore'); expect(coreNamespace).toBeDefined(); }); }); });