fluttertest_ai 0.3.1
fluttertest_ai: ^0.3.1 copied to clipboard
A local-first CLI that analyzes Flutter apps and generates conservative, runnable tests without an API key.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.3.1 - 2026-10-07 #
Fixed #
- Capped the
testdev-dependency to>=1.25.8 <1.31.2sopub getcannot resolvetest_api0.7.13+, which requires Dart SDK^3.11.0(language version 3.11). This restores a workingdart testtoolchain on Dart SDKs capped at language version 3.10.
0.3.0 - 2026-10-06 #
Added #
-
StateManagementAdaptergained five new members so an adapter can drive real test-environment setup instead of only wrapping a widget harness:appWidgetFor({required bool cupertino})(the root app widget name to pump inside),requiresBindingInit(whethersetUpAllneedsTestWidgetsFlutterBinding.ensureInitialized()),setUpStatements(...)/tearDownStatements()(raw Dart statements for the generatedsetUp/tearDown), andregistrationFor(RequiredDependency)(how to register one dependency). All five have safe no-op/default-MaterialAppdefaults, so every existing adapter still compiles unchanged.GetxAdapteris the first real implementation: it now pumpsGetMaterialApp/GetCupertinoApp, requires binding init, setsGet.testMode = trueand callsGet.reset()insetUp/tearDown, and registers aFake<TypeName>()for everyRequiredDependencywithmustBeRegistered == true(alphabetical, de-duplicated by type name). This targets the three largest failure classes seen against a real GetX project: "X not found" DI lookups, contextless-navigation null checks, and uninitialized-binding errors.GetxAdapterno longer extendsGenericAdapter. The generated test harness that consumes these five members (appWidgetFor/setUpStatements/tearDownStatements, and theFake<TypeName>class declarationsregistrationFordepends on) isTestHarnessBuilder, added below and wired into the generation pipeline in this same release. -
Added
TestHarnessBuilder(lib/src/generation/test_harness_builder.dart), which builds the source for a single shared, user-editabletest/fluttertest_ai_harness.dartfile instead of repeating environment setup in every generated test. For a givenProjectAnalysisandStateManagementAdapterit emits: aFake<TypeName>class (usingFakefrompackage:flutter_test, so nomockito/mocktaildependency is ever required in the target project) for every must-be-registered dependency with a resolvable import — types with no resolvable import are skipped and reported as a note rather than emitting a broken reference;setUpTestEnvironment()/tearDownTestEnvironment(), which callTestWidgetsFlutterBinding.ensureInitialized()when the adapter requires it, then a capped (10), best-effort, try/catch-guarded call to any structurally-matching static zero-arginitialize/init/ensureInitializedmethod found on a class whose name suggests global/app setup (theme, typography, tokens, config, locator, DI), then the adapter's own setup/teardown statements; andbuildTestApp(Widget child), which wrapschildinScaffoldinside the adapter's app widget (GetMaterialAppfor GetX, etc.), picking Cupertino only when strictly more analyzed files use it than Material. Output is deterministic for a given input. Wired into the generation pipeline later in this release — see below. -
DartSourceAnalyzernow detects dependency-injection requirements from unresolved AST syntax:Get.find<T>(),Get.put(...)/Get.lazyPut(...),Get.isRegistered<T>(), extendingGetView<T>/GetWidget<T>,context.read<T>()/ref.read<T>(),context.watch<T>()/ref.watch<T>(),Provider.of<T>(context), andBlocProvider.of<T>(context). Each finding is exposed as aRequiredDependency(type name,DependencyKind, and a best-effort resolvedimportUri) on the newDartFileInfo. requiredDependencies. This detection feeds bothTestHarnessBuilderandUnitTestGenerator, added later in this release. Added to close the single largest cause of generated-test failures observed against a real GetX project: calls likeGet.find<AuthService>()throw at runtime when nothing registeredAuthServicefirst, and the generator previously had no way to know a widget/controller needed a dependency registered before construction. -
TestHarnessBuilderis now wired into the generation pipeline:DartTestWriter.writeemitstest/fluttertest_ai_harness.dartonce per run, whenever the plan contains at least one widget test, obeying the same handwritten-preservation and--forcerules as every other generated file.WidgetTestGeneratornow imports the harness with a relative path computed from the test file's own location (correct at any nesting depth undertest/), callssetUp(setUpTestEnvironment)/tearDown(tearDownTestEnvironment), and pumps viabuildTestApp(Widget())instead of an inlineMaterialApp(home: ...)/adapter.wrapHarness(...)expression. The adapter's ownextraImports(e.g.package:get/get.dart) are no longer repeated in each generated widget test file, since the harness is now the only place that needs them. This closes the two largest GetX failure classes observed in the field (missing DI registration, missingGetMaterialAppancestor) by construction, for every generated widget test.
Fixed #
-
TestHarnessBuildercopied aRequiredDependency.importUristraight from the declaring source file's own import list, which may be relative ('../../data/services/auth_service.dart','profile_controller.dart'). A relative import is only meaningful relative to the file that wrote it; copied as-is intotest/fluttertest_ai_harness.dart, it resolved to a path that does not exist, so the harness failed to compile and every generated test that imported it failed to load. The harness builder now resolves each dependency's import by finding its declaring class among the analyzed project files and computing apackage:import from that file's own location (reusingtemplates.packageImport), falling back to the dependency's own best-effort import only when it is already apackage:URI, and otherwise skipping the dependency with a note. A relative import can no longer reach the harness;DartSourceAnalyzeritself also no longer records a relative import asimportUriat all, for the same reason.UnitTestGenerator's own harness-wireable check was updated to use the same project-wide resolution (previously it checked only the single file's own imports), so a dependency declared elsewhere in the project is not incorrectly treated as unresolvable. -
TestHarnessBuilderalways registered aFake<TypeName>for a must-be-registered GetX dependency, even when the real type was a plain, publicly-constructible class declared in the project.FakethrowsUnimplementedErroron every member access, so a widget depending on that fake (e.g. throughGetView.controller) failed with a different error instead of actually being testable. The harness builder now registers the real type (Get.put<T>(T(), permanent: true)) whenever its declaring class is in this project, publicly constructible, and not abstract, and falls back toFakeonly for external-package types, abstract types, or types with required constructor arguments.StateManagementAdapter.setUpStatements/registrationForgained an optionalDependencyRegistrationHint(instantiation expression + controller flag) so an adapter can make this choice;GetxAdapteralso now registers non-controller dependencies before controller (GetxController/GetxService) dependencies, since a controller's own field initializer may callGet.find<SomeService>()at construction time and would otherwise fail if the service were not registered yet. -
fixcould hang for an effectively unbounded amount of time on a large target project (observed: ~1500 generated test files, 50-100 MB offlutter testoutput). Four separate causes, all fixed together:GeneratedTestRepairer.repaircalledfailureOutput.contains(...)four times per candidate file, inside the per-file loop. With a multi-MBfailureOutputand hundreds of files this was effectivelyO(files * output length). The four checks are now computed once, before the loop, as hoisted boolean flags (RepairResultalso gains ascannedcount for the new per-attempt summary below).FlutterTestRunner.runusedProcess.run, which buffers all child output and prints nothing until the process exits — on a run that takes tens of minutes, the terminal looked frozen. It now usesProcess.startand streams stdout/stderr line-by-line through an optionalonProgress(String line)callback (both CLI commands passprint), draining both streams concurrently viaFuture.waitso neither the ~64 KB OS pipe buffer nor the child process can block.- Nothing bounded the run, so a wedged
flutter test(pending timers, tickers, contextless navigation — all plausible at this scale) hung forever.runandfixboth gained a--timeout <seconds>option (default 1800 = 30 minutes), implemented inFlutterTestRunner.runby racingprocess.exitCodeagainst the timeout, escalating fromSIGTERMtoSIGKILLafter a grace period, and returning aTestRunResultwith exit code124(never a false success) instead of throwing. - Retained process output is now capped to a 1 MB head + 1 MB tail (with
an elision marker noting how many characters/lines were dropped)
instead of holding, sanitizing, and re-encoding a 50-100 MB string
several times over.
flutter testis invoked with the directory form (flutter test test) instead of one argument per file once there are more than 200 generated files, or whenever every discovered test file is already a generated one, to avoid exceedingARG_MAX. Both the runner and the repairer now check only the first 4 KB of a file for the generated-ownership marker (it always lives in the file header) instead of reading the whole file, and walktest/withfollowLinks: false. fixnow printsAttempt N of M: scanned <n> file(s), repaired <n> file(s)before each repair pass's existing✓/–lines, so a long run shows visible progress instead of silence between attempts.
-
TestHarnessBuilderexcludes anyRequiredDependencywhosetypeNamestarts with_from both the emittedFake<TypeName>class and the container-registration list: a library-private type cannot be implemented (extends Fake implements _Foo) from the harness file, which lives in a different library, so this previously emitted non-compiling output. The exclusion is reported as a note. -
The unit-test generator addressed three failure classes measured from a real GetX project run (527 + 37 + 140 of 3013 observed failures):
ClassInfo.isFlutterStateis now true for a class extendingState,ConsumerState, orHookState(matched on the superclass, never the class's own name, so an unrelated domain class merely named...Stateis unaffected).LogicAnalyzer.pureMethodClasses/lifecycleClassesexclude these —initState/disposecan never work outside the element tree that creates aState— andLogicAnalyzer.flutterStateSkippedClassesreports the skip, surfaced byTestPlanBuilderinto the plan'sskippedlist.- A class whose file requires an unregistered dependency-injection type
(
Get.find<T>(),context.read<T>(), ...) is no longer constructed bare.UnitTestGeneratornow wires the shared harness'ssetUpTestEnvironment/tearDownTestEnvironment(imported with a relative path computed by the newtemplates.harnessImport, shared with the widget generator) when every must-register dependency has a resolvable, public fake to register, and otherwise skips the controller/state-container instance tests with a note naming the dependency — registering only some of them would just trade one failure for another. Separately,onInit/onReady/onCloseon aGetxController/GetxService/Bloc/Cubitare never turned into a bareController().onInit()smoke test, since those hooks only fire meaningfully under a framework-driven lifecycle; a plain synchronous method on the same controller remains testable. - A callable name-shaped like a JSON decoder (
*FromJson,*FromJsonString,decode*) taking a singleStringparameter is no longer called with'sample'or''— both are guaranteed-invalid JSON andjsonDecodethrows on either.ValueFixturesnow recognises this shape and supplies a confidently-valid minimal literal ('{}', or'[]'when the return type is list-shaped) for the typical-input case, and reuses the same tolerant accept-either-outcome assertion_fromJsonTestalready uses (0% failure rate) for the empty-input case.
0.2.1 #
Fixed #
- Widget-test generation asserted that every static string found inside a
Text(...)was visible immediately after the firstpump(), with no check for whether thatTextwas conditionally rendered. Any text behind a collection-if, anif/else, a ternary, a??, a loop, or aswitchproduced afind.text(...)assertion that failed the instant the gating condition started out false (e.g.if (_submitted) Text('Signed in')when_submittedstartsfalse) — a pattern common to conditionally-rendered error/empty/loading/success states in real apps.DartSourceAnalyzernow walks the AST ancestor chain of every candidate node up to its enclosing function/method body, and only feedsvisibleTexts,keys,buttonTypes,textFieldTypes/hasTextField,hasForm, andloadingIndicatorswhen the node is unconditionally built. The same conditional-render bug affected keys (findsOneWidgetis even stricter than the text case'sfindsWidgets), buttons, text fields, forms, and loading indicators, so all six signals are fixed together rather than only the text case.
0.2.0 #
Corrective release. 0.1.0 generated no unit tests at all, and only a single "renders without crashing" widget test per screen. Both generation paths are fixed and verified end to end against real Flutter applications.
Fixed #
- State-management detection now matches structured, tokenised evidence
(exact dependency names, exact
package:import package-name segments, exactClassInfo.superclassNamematches, exactcreationNameswidget constructors) instead of substring-matching a lowercased blob of all imports/class names concatenated together. The previous approach let the GetX tokengetmatchpackage:flutter/widgets.dart(getis a substring of "widgets"), misclassifying any Flutter project as GetX. Confidence now scales with the number and diversity of independent evidence kinds (dependency,import,class inheritance,widget usage,behavior), and precedence between competing frameworks is resolved deterministically by comparing confidence rather than by hardcoded map iteration order. setStateis no longer inferred merely from "a widget class exists somewhere in the project" — it now requires real evidence (DartFileInfo.usesSetState, and/or a class extendingState). Projects with onlyStatelessWidgets and nosetState()calls now correctly resolve tounknown/genericAdapter.- Widget test generator now consumes the already-computed source analysis (visible text, keys, button/text-field/form/loading signals) instead of re-parsing source and emitting only a render smoke test. Generated tests now assert on static UI text, stable keys, button/text-field presence, loading indicators, and (when unambiguous) input/tap/form-submit interactions, using only stable selectors (keys, text, types).
- Widget test harness selection now delegates to the adapter registry
(
StateManagementAdapters.forFramework) instead of a hardcoded Riverpod check, so all nine state-management adapters are used for widget-test harnesses, not just Riverpod and the generic fallback. - Widget tests now use the
generatedRegionBegin/generatedRegionEndmarkers, so--forcecorrectly refreshes only the generated region. - Widgets that read inherited state unsafely for their detected framework
(
WidgetAnalyzer.requiresUnsafeState) are now actually skipped with an explanatory note, matching the behaviour already documented in report limitations text. - Fixed a source-analysis gap where widget/form/button/loading-indicator
detection only matched
const/new-prefixed constructor calls; bare calls (e.g.ElevatedButton(onPressed: _increment, ...), which cannot beconstwhen an argument like an instance-method tear-off isn't a compile-time constant) were invisible to analysis. This affected nearly all real-world widget code, since most non-trivial widget instantiations aren'tconst. - Unit-test generation was fixed so it reliably produces a compiling test
file for every eligible target (typed top-level functions, static and
instance methods, getters,
fromJsonfactories, and state-container lifecycle smoke tests), synthesizing typical, empty, and null-input argument lists only where a safe value is known, and deduplicating argument lists that would otherwise produce byte-identical test bodies. - Generated unit-test files now import
package:flutter_test/flutter_test.dartdirectly, so a generated unit test runs standalone with zero changes to the target project'spubspec.yaml, instead of depending on an import that generation previously omitted. - Unit-test target selection (
LogicAnalyzer.pureMethodClasses) was broadened from gating eligibility on a class name matching a conventional utility-class pattern (Validator,Formatter,Mapper,Converter,Parser,Helper,Calculator,Utils/Tools) to a structural check: a class is now an eligible unit-test target if it is publicly constructible, is not a widget, and exposes at least one public, synchronous, non-operator method or getter with synthesizable parameters — regardless of its name. Naming convention is now only a confidence signal (hasConventionalName), not a gate. Classes that are structurally eligible but whose name or file imports strongly suggest external I/O (Service,Repository,Client,Api,Http,Database,Db,Gateway,DataSource; or an import ofdart:io,http,dio,shared_preferences,sqflite, orfirebase) are excluded and reported via a recorded skip reason instead of being silently dropped. - Deterministic repair of a
BlocBuilderwidget test missing itsBlocProviderancestor was fixed: the repairer now extracts the bloc's type from theBlocBuilder<...>generic, verifies the type is actually constructible from the source file before rewriting anything, wraps the harness inBlocProvider<T>(create: (_) => T(), child: ...), and normalizes anypackage:<project>/lib/...import produced by generation down to the correctpackage:<project>/...form.
0.1.0 - 2026-09-13 #
Added #
- Initial local-first Flutter test analysis, generation, execution, repair, and reporting MVP
- CLI commands:
doctor,analyze,generate,run,fix,report - AST-based source analysis using the Dart
analyzerpackage - State management detection for: setState, Provider, Riverpod, BLoC/Cubit, GetX, Redux, MobX, ValueNotifier, Signals, and custom/unknown
- Dedicated test harness adapters for each supported state management approach
- Generic fallback for unknown state management with behavior-based widget tests
- Unit test generation for: pure functions, validators, formatters, JSON models, utility classes, and state containers
- Widget test generation with proper test harnesses (MaterialApp, ProviderScope, BlocProvider, etc.)
- Conservative test plan with confidence scores and skip reasons
- Safe test writer that never modifies
lib/or overwrites handwritten tests - Generated-test ownership marker for safe refresh and repair
flutter testexecution with machine-readable (JSON events) and human-readable output parsing- Deterministic test repair for common failures (missing imports, harness wrappers, finder issues)
- Sanitized report generation (Markdown and JSON) with credential redaction
- Optional AI provider interface with disabled-by-default implementation
- OpenAI-compatible provider stub (opt-in, disabled by default)
- Comprehensive test suite with fixture projects for each supported state management approach
- Support for Windows, macOS, and Linux