Skip to main content
Glama
testGenerator.js13.7 kB
import { parseFlutterCode } from '../utils/parser.js'; import { analyzeCodeStructure } from '../utils/codeAnalyzer.js'; export async function generateWidgetTest(args) { const { widgetCode, testFramework = 'flutter_test', includeGoldenTests = false } = args; try { const parsedCode = parseFlutterCode(widgetCode); const structure = analyzeCodeStructure(widgetCode); const testSuite = { imports: generateTestImports(parsedCode, testFramework), setup: generateTestSetup(parsedCode), tests: [], goldenTests: [], }; // Generate widget existence tests testSuite.tests.push(...generateExistenceTests(parsedCode)); // Generate interaction tests if (structure.hasInteractiveWidgets) { testSuite.tests.push(...generateInteractionTests(widgetCode, parsedCode)); } // Generate state tests for StatefulWidgets if (parsedCode.classes.some(c => c.isStateful)) { testSuite.tests.push(...generateStateTests(widgetCode, parsedCode)); } // Generate edge case tests testSuite.tests.push(...generateEdgeCaseTests(parsedCode)); // Generate golden tests if requested if (includeGoldenTests) { testSuite.goldenTests = generateGoldenTests(parsedCode); } // Generate accessibility tests testSuite.tests.push(...generateAccessibilityTests(parsedCode)); const generatedCode = formatTestCode(testSuite); const coverage = estimateTestCoverage(testSuite, parsedCode); return { content: [ { type: 'text', text: JSON.stringify({ testCode: generatedCode, coverage, testCount: testSuite.tests.length + testSuite.goldenTests.length, recommendations: generateTestRecommendations(parsedCode, structure), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error generating widget tests: ${error.message}`, }, ], }; } } function generateTestImports(parsedCode, framework) { const imports = [ `import 'package:${framework}/flutter_test.dart';`, `import 'package:flutter/material.dart';`, ]; // Add imports based on widget usage if (parsedCode.imports.some(i => i.includes('provider'))) { imports.push(`import 'package:provider/provider.dart';`); } return imports; } function generateTestSetup(parsedCode) { const mainWidget = parsedCode.classes[0]?.name || 'MyWidget'; return ` Widget createWidgetUnderTest() { return MaterialApp( home: Scaffold( body: ${mainWidget}(), ), ); }`; } function generateExistenceTests(parsedCode) { const tests = []; const widgetName = parsedCode.classes[0]?.name || 'MyWidget'; tests.push({ name: `${widgetName} builds without error`, code: ` testWidgets('${widgetName} builds without error', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); expect(find.byType(${widgetName}), findsOneWidget); });`, }); // Test for specific widgets found in the code const commonWidgets = ['Text', 'Container', 'Column', 'Row', 'Button']; parsedCode.widgets.forEach(widget => { if (commonWidgets.some(w => widget.includes(w))) { tests.push({ name: `finds ${widget} widget`, code: ` testWidgets('finds ${widget} widget', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); expect(find.byType(${widget}), findsWidgets); });`, }); } }); return tests; } function generateInteractionTests(code, parsedCode) { const tests = []; // Test button taps if (code.includes('onPressed') || code.includes('onTap')) { tests.push({ name: 'handles tap interactions', code: ` testWidgets('handles tap interactions', (WidgetTester tester) async { bool tapped = false; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (context) => YourWidget( onTap: () => tapped = true, ), ), ), ), ); await tester.tap(find.byType(InkWell).first); await tester.pump(); expect(tapped, isTrue); });`, }); } // Test text input if (code.includes('TextField') || code.includes('TextFormField')) { tests.push({ name: 'handles text input', code: ` testWidgets('handles text input', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); await tester.enterText(find.byType(TextField).first, 'Test input'); await tester.pump(); expect(find.text('Test input'), findsOneWidget); });`, }); } // Test scrolling if (code.includes('ScrollView') || code.includes('ListView')) { tests.push({ name: 'handles scrolling', code: ` testWidgets('handles scrolling', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); await tester.drag(find.byType(ListView).first, const Offset(0, -300)); await tester.pump(); // Verify scroll position changed });`, }); } return tests; } function generateStateTests(code, parsedCode) { const tests = []; const statefulWidget = parsedCode.classes.find(c => c.isStateful); if (statefulWidget) { tests.push({ name: 'updates state correctly', code: ` testWidgets('updates state correctly', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); // Initial state expect(find.text('0'), findsOneWidget); // Trigger state change await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify state updated expect(find.text('1'), findsOneWidget); });`, }); // Test setState behavior if (code.includes('setState')) { tests.push({ name: 'setState triggers rebuild', code: ` testWidgets('setState triggers rebuild', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); final initialBuildCount = tester.element(find.byType(${statefulWidget.name})).widget.hashCode; await tester.tap(find.byType(ElevatedButton).first); await tester.pump(); final afterBuildCount = tester.element(find.byType(${statefulWidget.name})).widget.hashCode; expect(initialBuildCount, isNot(equals(afterBuildCount))); });`, }); } } return tests; } function generateEdgeCaseTests(parsedCode) { const tests = []; tests.push({ name: 'handles null or empty data', code: ` testWidgets('handles null or empty data', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: YourWidget(data: null), ), ), ); expect(tester.takeException(), isNull); });`, }); tests.push({ name: 'handles extremely long text', code: ` testWidgets('handles extremely long text', (WidgetTester tester) async { final longText = 'A' * 1000; await tester.pumpWidget( MaterialApp( home: Scaffold( body: YourWidget(text: longText), ), ), ); expect(find.text(longText), findsOneWidget); expect(tester.takeException(), isNull); });`, }); tests.push({ name: 'handles rapid interactions', code: ` testWidgets('handles rapid interactions', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); // Rapidly tap button for (int i = 0; i < 10; i++) { await tester.tap(find.byType(ElevatedButton).first); } await tester.pump(); expect(tester.takeException(), isNull); });`, }); return tests; } function generateGoldenTests(parsedCode) { const goldenTests = []; const widgetName = parsedCode.classes[0]?.name || 'MyWidget'; goldenTests.push({ name: 'matches golden file', code: ` testWidgets('${widgetName} matches golden file', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); await expectLater( find.byType(${widgetName}), matchesGoldenFile('goldens/${widgetName.toLowerCase()}.png'), ); });`, }); // Golden tests for different states goldenTests.push({ name: 'matches golden file in dark mode', code: ` testWidgets('${widgetName} matches golden file in dark mode', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData.dark(), home: ${widgetName}(), ), ); await expectLater( find.byType(${widgetName}), matchesGoldenFile('goldens/${widgetName.toLowerCase()}_dark.png'), ); });`, }); // Different screen sizes goldenTests.push({ name: 'matches golden file on tablet', code: ` testWidgets('${widgetName} matches golden file on tablet', (WidgetTester tester) async { tester.binding.window.physicalSizeTestValue = const Size(1024, 768); tester.binding.window.devicePixelRatioTestValue = 1.0; await tester.pumpWidget(createWidgetUnderTest()); await expectLater( find.byType(${widgetName}), matchesGoldenFile('goldens/${widgetName.toLowerCase()}_tablet.png'), ); });`, }); return goldenTests; } function generateAccessibilityTests(parsedCode) { const tests = []; tests.push({ name: 'meets accessibility guidelines', code: ` testWidgets('meets accessibility guidelines', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(createWidgetUnderTest()); // Check for semantic labels expect(tester.getSemantics(find.byType(Image)), isNotNull); // Check for button labels final buttons = find.byType(ElevatedButton); if (buttons.evaluate().isNotEmpty) { expect( tester.getSemantics(buttons.first), matchesSemantics(label: isNotNull), ); } handle.dispose(); });`, }); tests.push({ name: 'supports screen readers', code: ` testWidgets('supports screen readers', (WidgetTester tester) async { await tester.pumpWidget(createWidgetUnderTest()); // Verify important elements have semantic labels final semantics = tester.getSemantics(find.byType(Scaffold)); expect(semantics.label, isNotNull); });`, }); return tests; } function formatTestCode(testSuite) { let code = ''; // Add imports code += testSuite.imports.join('\n') + '\n\n'; // Add main test function code += 'void main() {\n'; // Add setup code += testSuite.setup + '\n'; // Add regular tests code += '\n group(\'Widget Tests\', () {\n'; testSuite.tests.forEach(test => { code += test.code + '\n'; }); code += ' });\n'; // Add golden tests if any if (testSuite.goldenTests.length > 0) { code += '\n group(\'Golden Tests\', () {\n'; testSuite.goldenTests.forEach(test => { code += test.code + '\n'; }); code += ' });\n'; } code += '}\n'; return code; } function estimateTestCoverage(testSuite, parsedCode) { const totalTests = testSuite.tests.length + testSuite.goldenTests.length; const widgetCount = parsedCode.widgets.length; const methodCount = parsedCode.methods.length; const coverage = { estimatedPercentage: Math.min(95, (totalTests / (widgetCount + methodCount)) * 100), coveredAreas: [], uncoveredAreas: [], }; // Determine covered areas if (testSuite.tests.some(t => t.name.includes('builds without error'))) { coverage.coveredAreas.push('Widget construction'); } if (testSuite.tests.some(t => t.name.includes('interaction'))) { coverage.coveredAreas.push('User interactions'); } if (testSuite.tests.some(t => t.name.includes('state'))) { coverage.coveredAreas.push('State management'); } if (testSuite.goldenTests.length > 0) { coverage.coveredAreas.push('Visual regression'); } // Determine uncovered areas if (!testSuite.tests.some(t => t.name.includes('performance'))) { coverage.uncoveredAreas.push('Performance testing'); } if (!testSuite.tests.some(t => t.name.includes('animation'))) { coverage.uncoveredAreas.push('Animation testing'); } return coverage; } function generateTestRecommendations(parsedCode, structure) { const recommendations = []; if (structure.hasBusinessLogicInUI) { recommendations.push({ type: 'architecture', message: 'Consider extracting business logic to make testing easier', priority: 'high', }); } if (!parsedCode.methods.some(m => m.name.includes('test'))) { recommendations.push({ type: 'testing', message: 'Add unit tests for individual methods', priority: 'medium', }); } if (structure.hasDataFetching) { recommendations.push({ type: 'mocking', message: 'Use mock data providers for network requests in tests', priority: 'high', }); } recommendations.push({ type: 'integration', message: 'Consider adding integration tests for complete user flows', priority: 'medium', }); return recommendations; }

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/dvillegastech/flutter_mcp_2'

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