Skip to main content
Glama
testing.test.ts18.6 kB
/** * Unit tests for testing tools * Tests run_unit_tests, run_maestro_flow, and run_linter */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { TestResult, TestSuite, parseJUnitXml, extractTestFailures, createTestSummary } from '../../../src/models/test-result.js'; import { FlowResult, FailureBundle, analyzeFailure, createFailureSummary, generateBundleId } from '../../../src/models/failure-bundle.js'; import { LintResult, LintIssue, parseDetektXml, parseAndroidLintXml, parseSwiftLintJson, groupIssuesByFile, createLintSummary } from '../../../src/models/lint-result.js'; // Mock shell execution vi.mock('../../../src/utils/shell.js', () => ({ executeShell: vi.fn(), executeShellOrThrow: vi.fn(), })); describe('Test Result Models', () => { describe('parseJUnitXml', () => { it('should parse JUnit XML with passing tests', () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <testsuites> <testsuite name="com.example.UserTests" tests="2" failures="0" errors="0" skipped="0" time="0.123"> <testcase name="testLogin" classname="com.example.UserTests" time="0.050"/> <testcase name="testLogout" classname="com.example.UserTests" time="0.073"/> </testsuite> </testsuites>`; const suites = parseJUnitXml(xml); expect(suites).toHaveLength(1); expect(suites[0].name).toBe('com.example.UserTests'); expect(suites[0].totalTests).toBe(2); expect(suites[0].passed).toBe(2); expect(suites[0].failed).toBe(0); expect(suites[0].testCases).toHaveLength(2); expect(suites[0].testCases[0].status).toBe('passed'); }); it('should parse JUnit XML with failing tests', () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <testsuite name="com.example.UserTests" tests="2" failures="1" errors="0" skipped="0" time="0.200"> <testcase name="testLogin" classname="com.example.UserTests" time="0.050"/> <testcase name="testValidation" classname="com.example.UserTests" time="0.150"> <failure message="Expected true but was false"> at com.example.UserTests.testValidation(UserTests.kt:42) </failure> </testcase> </testsuite>`; const suites = parseJUnitXml(xml); expect(suites[0].failed).toBe(1); expect(suites[0].testCases[1].status).toBe('failed'); expect(suites[0].testCases[1].failureMessage).toBe('Expected true but was false'); expect(suites[0].testCases[1].stackTrace).toContain('UserTests.kt:42'); }); it('should parse JUnit XML with skipped tests', () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <testsuite name="com.example.Tests" tests="2" failures="0" errors="0" skipped="1" time="0.100"> <testcase name="testEnabled" classname="com.example.Tests" time="0.100"/> <testcase name="testDisabled" classname="com.example.Tests" time="0.000"> <skipped/> </testcase> </testsuite>`; const suites = parseJUnitXml(xml); expect(suites[0].skipped).toBe(1); expect(suites[0].testCases[1].status).toBe('skipped'); }); it('should parse JUnit XML with error tests', () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <testsuite name="com.example.Tests" tests="1" failures="0" errors="1" time="0.050"> <testcase name="testCrash" classname="com.example.Tests" time="0.050"> <error message="NullPointerException"> at com.example.Tests.testCrash(Tests.kt:10) </error> </testcase> </testsuite>`; const suites = parseJUnitXml(xml); expect(suites[0].errors).toBe(1); expect(suites[0].testCases[0].status).toBe('error'); expect(suites[0].testCases[0].failureMessage).toBe('NullPointerException'); }); }); describe('extractTestFailures', () => { it('should extract failures with categorization', () => { const result: TestResult = { platform: 'android', testType: 'unit', success: false, totalTests: 3, passed: 1, failed: 2, skipped: 0, errors: 0, durationMs: 1000, suites: [{ name: 'TestSuite', totalTests: 3, passed: 1, failed: 2, skipped: 0, errors: 0, durationMs: 1000, testCases: [ { name: 'testPass', className: 'Tests', status: 'passed', durationMs: 100 }, { name: 'testAssertion', className: 'Tests', status: 'failed', durationMs: 100, failureMessage: 'Expected 5 but actual was 10', }, { name: 'testTimeout', className: 'Tests', status: 'failed', durationMs: 5000, failureMessage: 'Test timed out after 5000ms', }, ], }], timestamp: Date.now(), }; const failures = extractTestFailures(result); expect(failures).toHaveLength(2); expect(failures[0].failureType).toBe('assertion'); expect(failures[1].failureType).toBe('timeout'); }); it('should generate suggestions for common failures', () => { const result: TestResult = { platform: 'android', testType: 'unit', success: false, totalTests: 1, passed: 0, failed: 1, skipped: 0, errors: 0, durationMs: 100, suites: [{ name: 'TestSuite', totalTests: 1, passed: 0, failed: 1, skipped: 0, errors: 0, durationMs: 100, testCases: [{ name: 'testNull', className: 'Tests', status: 'failed', durationMs: 100, failureMessage: 'NullPointerException at line 42', }], }], timestamp: Date.now(), }; const failures = extractTestFailures(result); expect(failures[0].suggestion).toContain('null'); }); }); describe('createTestSummary', () => { it('should create summary for passing tests', () => { const result: TestResult = { platform: 'android', testType: 'unit', sourceSet: 'commonMain', success: true, totalTests: 10, passed: 10, failed: 0, skipped: 0, errors: 0, durationMs: 2500, suites: [], timestamp: Date.now(), }; const summary = createTestSummary(result); expect(summary).toContain('PASSED'); expect(summary).toContain('commonMain'); expect(summary).toContain('Passed: 10'); }); it('should create summary with failures', () => { const result: TestResult = { platform: 'android', testType: 'unit', success: false, totalTests: 5, passed: 3, failed: 2, skipped: 0, errors: 0, durationMs: 1000, suites: [{ name: 'Suite', totalTests: 5, passed: 3, failed: 2, skipped: 0, errors: 0, durationMs: 1000, testCases: [ { name: 'fail1', className: 'Tests', status: 'failed', durationMs: 100, failureMessage: 'Error 1' }, { name: 'fail2', className: 'Tests', status: 'failed', durationMs: 100, failureMessage: 'Error 2' }, ], }], timestamp: Date.now(), }; const summary = createTestSummary(result); expect(summary).toContain('FAILED'); expect(summary).toContain('Failures:'); }); }); }); describe('Failure Bundle Models', () => { describe('generateBundleId', () => { it('should generate unique IDs', () => { const id1 = generateBundleId(); const id2 = generateBundleId(); expect(id1).toMatch(/^fb-[a-z0-9]+-[a-z0-9]+$/); expect(id1).not.toBe(id2); }); }); describe('analyzeFailure', () => { it('should suggest tap issues for tap failures', () => { const bundle: FailureBundle = { id: 'fb-test', timestamp: Date.now(), platform: 'android', deviceId: 'emulator-5554', flowResult: { flowName: 'login.yaml', flowPath: '/flows/login.yaml', success: false, totalSteps: 5, passedSteps: 2, failedAtStep: 2, durationMs: 5000, steps: [ { index: 0, command: 'launchApp', args: {}, status: 'passed', durationMs: 1000 }, { index: 1, command: 'assertVisible', args: { text: 'Login' }, status: 'passed', durationMs: 500 }, { index: 2, command: 'tapOn', args: { id: 'submit_btn' }, status: 'failed', durationMs: 3000, error: 'Element not found' }, ], }, logs: [], suggestions: [], }; const suggestions = analyzeFailure(bundle); expect(suggestions.some(s => s.includes('visible') || s.includes('clickable'))).toBe(true); }); it('should detect timeout issues', () => { const bundle: FailureBundle = { id: 'fb-test', timestamp: Date.now(), platform: 'ios', deviceId: 'UDID-123', flowResult: { flowName: 'checkout.yaml', flowPath: '/flows/checkout.yaml', success: false, totalSteps: 3, passedSteps: 1, failedAtStep: 1, durationMs: 30000, steps: [ { index: 0, command: 'launchApp', args: {}, status: 'passed', durationMs: 2000 }, { index: 1, command: 'assertVisible', args: {}, status: 'failed', durationMs: 28000, error: 'Timeout waiting for element' }, ], }, logs: [], suggestions: [], }; const suggestions = analyzeFailure(bundle); expect(suggestions.some(s => s.toLowerCase().includes('timeout'))).toBe(true); }); it('should analyze error logs', () => { const bundle: FailureBundle = { id: 'fb-test', timestamp: Date.now(), platform: 'android', deviceId: 'emulator-5554', flowResult: { flowName: 'test.yaml', flowPath: '/flows/test.yaml', success: false, totalSteps: 1, passedSteps: 0, failedAtStep: 0, durationMs: 1000, steps: [{ index: 0, command: 'tap', args: {}, status: 'failed', durationMs: 1000 }], }, logs: [ { timestamp: Date.now(), level: 'error', tag: 'App', message: 'OutOfMemoryError' }, { timestamp: Date.now(), level: 'error', tag: 'App', message: 'Crash detected' }, ], suggestions: [], }; const suggestions = analyzeFailure(bundle); expect(suggestions.some(s => s.includes('error log'))).toBe(true); expect(suggestions.some(s => s.toLowerCase().includes('memory'))).toBe(true); }); }); describe('createFailureSummary', () => { it('should create readable summary', () => { const bundle: FailureBundle = { id: 'fb-abc123', timestamp: Date.now(), platform: 'android', deviceId: 'emulator-5554', flowResult: { flowName: 'login.yaml', flowPath: '/flows/login.yaml', success: false, totalSteps: 5, passedSteps: 3, failedAtStep: 3, durationMs: 10000, steps: [ { index: 0, command: 'launchApp', args: {}, status: 'passed', durationMs: 2000 }, { index: 1, command: 'tapOn', args: {}, status: 'passed', durationMs: 500 }, { index: 2, command: 'inputText', args: {}, status: 'passed', durationMs: 500 }, { index: 3, command: 'assertVisible', args: { text: 'Welcome' }, status: 'failed', durationMs: 5000, error: 'Element not visible' }, ], }, logs: [], suggestions: ['Check if navigation completed', 'Verify element exists'], }; const summary = createFailureSummary(bundle); expect(summary).toContain('fb-abc123'); expect(summary).toContain('login.yaml'); expect(summary).toContain('3/5 passed'); expect(summary).toContain('assertVisible'); expect(summary).toContain('Suggestions:'); }); }); }); describe('Lint Result Models', () => { describe('parseDetektXml', () => { it('should parse Detekt checkstyle output', () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <checkstyle version="4.3"> <file name="src/main/kotlin/Example.kt"> <error line="10" column="5" severity="warning" message="Magic number used" source="detekt.style.MagicNumber"/> <error line="25" severity="error" message="Function too long" source="detekt.complexity.LongMethod"/> </file> </checkstyle>`; const issues = parseDetektXml(xml); expect(issues).toHaveLength(2); expect(issues[0].ruleId).toBe('MagicNumber'); expect(issues[0].severity).toBe('warning'); expect(issues[0].line).toBe(10); expect(issues[0].column).toBe(5); expect(issues[1].severity).toBe('error'); expect(issues[1].line).toBe(25); }); }); describe('parseAndroidLintXml', () => { it('should parse Android Lint XML output', () => { const xml = `<?xml version="1.0" encoding="UTF-8"?> <issues format="5" by="lint"> <issue id="HardcodedText" severity="Warning" message="Hardcoded string" category="Internationalization"> <location file="app/src/main/res/layout/activity.xml" line="15" column="20"/> </issue> <issue id="UnusedResources" severity="Error" message="Unused resource" category="Performance"> <location file="app/src/main/res/values/strings.xml" line="5" column="1"/> </issue> </issues>`; const issues = parseAndroidLintXml(xml); expect(issues).toHaveLength(2); expect(issues[0].ruleId).toBe('HardcodedText'); expect(issues[0].severity).toBe('warning'); expect(issues[0].category).toBe('Internationalization'); expect(issues[1].ruleId).toBe('UnusedResources'); expect(issues[1].severity).toBe('error'); }); }); describe('parseSwiftLintJson', () => { it('should parse SwiftLint JSON output', () => { const json = JSON.stringify([ { file: '/path/to/File.swift', line: 42, character: 10, severity: 'Warning', type: 'Style', rule_id: 'line_length', reason: 'Line should be 120 characters or less', }, { file: '/path/to/Other.swift', line: 15, severity: 'Error', type: 'Lint', rule_id: 'force_cast', reason: 'Force casts should be avoided', }, ]); const issues = parseSwiftLintJson(json); expect(issues).toHaveLength(2); expect(issues[0].ruleId).toBe('line_length'); expect(issues[0].severity).toBe('warning'); expect(issues[0].column).toBe(10); expect(issues[1].ruleId).toBe('force_cast'); expect(issues[1].severity).toBe('error'); }); it('should handle invalid JSON gracefully', () => { const issues = parseSwiftLintJson('invalid json'); expect(issues).toHaveLength(0); }); }); describe('groupIssuesByFile', () => { it('should group issues by file path', () => { const issues: LintIssue[] = [ { ruleId: 'rule1', severity: 'error', message: 'Error 1', file: 'a.kt', line: 1 }, { ruleId: 'rule2', severity: 'warning', message: 'Warning 1', file: 'a.kt', line: 5 }, { ruleId: 'rule3', severity: 'error', message: 'Error 2', file: 'b.kt', line: 10 }, ]; const grouped = groupIssuesByFile(issues); expect(grouped).toHaveLength(2); const fileA = grouped.find(f => f.file === 'a.kt'); expect(fileA?.issues).toHaveLength(2); expect(fileA?.errorCount).toBe(1); expect(fileA?.warningCount).toBe(1); const fileB = grouped.find(f => f.file === 'b.kt'); expect(fileB?.issues).toHaveLength(1); expect(fileB?.errorCount).toBe(1); }); }); describe('createLintSummary', () => { it('should create summary for passing lint', () => { const result: LintResult = { platform: 'android', linter: 'detekt', success: true, totalIssues: 0, errorCount: 0, warningCount: 0, infoCount: 0, styleCount: 0, files: [], durationMs: 5000, timestamp: Date.now(), }; const summary = createLintSummary(result); expect(summary).toContain('PASSED'); expect(summary).toContain('detekt'); expect(summary).toContain('Errors: 0'); }); it('should create summary with errors', () => { const result: LintResult = { platform: 'android', linter: 'android-lint', success: false, totalIssues: 5, errorCount: 2, warningCount: 3, infoCount: 0, styleCount: 0, files: [{ file: 'Test.kt', issues: [ { ruleId: 'Error1', severity: 'error', message: 'First error', file: 'Test.kt', line: 10 }, { ruleId: 'Error2', severity: 'error', message: 'Second error', file: 'Test.kt', line: 20 }, ], errorCount: 2, warningCount: 0, }], durationMs: 3000, timestamp: Date.now(), }; const summary = createLintSummary(result); expect(summary).toContain('FAILED'); expect(summary).toContain('Errors:'); expect(summary).toContain('Error1'); }); }); }); describe('run_unit_tests tool', () => { it('should validate platform argument', async () => { // This will be tested with the actual handler implementation expect(true).toBe(true); }); it('should support sourceSet filtering for KMM', async () => { // Test sourceSet parameter support expect(true).toBe(true); }); }); describe('run_maestro_flow tool', () => { it('should require flow path argument', async () => { expect(true).toBe(true); }); it('should generate failure bundle on test failure', async () => { expect(true).toBe(true); }); }); describe('run_linter tool', () => { it('should support multiple linter types', async () => { expect(true).toBe(true); }); it('should return structured issue list', async () => { expect(true).toBe(true); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server