NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #3380 most downloaded on pub.dev
Flutter widget test toolkit - spot, act, validate. Better selectors, automatic screenshots, chainable.
Last release 2 days ago
05 Oct 2026
Ships unpredictably
gaps range from 2 weeks to 1.2 years
Nearly every release is documented
notes for 28 of 28 stable releases
Nothing withdrawn
no release was ever pulled
4 years old
33 releases · first in 2022
One column per quarter.
New: Tests using spot can be compiled to WebAssembly with flutter test --platform chrome --wasm , which until now failed to compile for any test impor
Breaking: spot now requires Dart 3.4 / Flutter 3.22. Jaspr, which renders the timeline report, is updated to 0.17.1. #167
spot-testing AI agent skill for consumers to install with dart run skills@ get --package spot. #172Fix: act.tap() and act.tapAt() now use a fresh pointer id per tap instead of always reusing pointer 0. Previously, when an earlier test left a gesture
act.tap() and act.tapAt() now use a fresh pointer id per tap instead of always reusing pointer 0. Previously, when an earlier test left a gesture arena unresolved, the next tap joined that stale arena and was silently swallowed. #165 (thx @peter-trost)async_patch.dart instead. #166Deprecated: spotText(text, exact: true) is now spotText(text, whole: true) — the flag controls whole-string vs. substring matching, not character hand…
This release is mostly about speed. Two changes carry it:
withParent, withChild, chained selectors) are up to 200x faster on big widget trees. Relationships are now resolved by walking up the tree instead of searching the subtree of every parent. #148The rest:
hasDiagnosticProp, getDiagnosticProp and withDiagnosticProp now cache debugFillProperties. ~1.5x faster #159act.tapAt() timeline events and the diagnostics behind failing act.tap() calls. #154spotKey(key).existsOnce() were extremely slow (tens of seconds) when no match was found in a large widget tree. The error output is now limited and match-all selectors are no longer suggested as "less specific" matches. #119New: act.inspectTap() reports whether a widget can be tapped and why not, as a value instead of a thrown error #150
final inspection = act.inspectTap(spot<ElevatedButton>());
expect(inspection.canTap, isFalse);
// the button is behind a full-screen overlay
expect(
inspection.tapFailure?.tapCoveredReason.primaryCover?.widget,
isA<ColoredBox>(),
);Available reasons: TapNotFoundReason, TapMultipleWidgetsFoundReason, TapNoRenderObjectReason, TapNonRenderBoxReason, TapOutsideViewportReason, TapAbsorbedReason, TapIgnoredReason, TapOffstageReason, TapZeroSizeReason, TapCoveredReason and TapUnknownReason. Also add TapInspection, TapFailureReason, TapWidgetInfo, TapHitTestInfo, TapHitSample, TapSamples and TapBlocker.
TapInspection.samples reports how much of the widget reacts to pointer events and what is in the way, for tappable widgets too. A widget that is tappable but only partially reachable has no failure to assert on, so assert on the samples.
final samples = act.inspectTap(spot<ElevatedButton>()).samples!;
print('${samples.hittablePercent}% of the button reacts to taps');
for (final blocker in samples.blockers) {
print('${blocker.receiver.widgetName} covers ${blocker.percent}%');
}Sampling hit tests the whole widget on a grid, which costs far more than the rest of the inspection, so it happens on the first read of samples instead of up front. An inspection describes the tree of the frame it was created in, so reading samples after a pump throws instead of reporting what a different tree does.
New: act.tap() throws a TapFailure that carries the TapInspection explaining the failure, so the reason can be asserted without matching on the message. TapFailure extends TestFailure, existing expectations keep working. #150
await expectLater(
() => act.tap(spot<ElevatedButton>()),
throwsA(
isA<TapFailure>().having(
(it) => it.inspection.tapFailure?.reason,
'reason',
isA<TapCoveredReason>(),
),
),
);Fix: act.tap() now finds an AbsorbPointer anywhere above the target. It previously only looked directly below the widget #150
Fix: act.tap() now reports the outermost AbsorbPointer or IgnorePointer above the target instead of the closest one #150
New: act.tap() explains offstage widgets instead of reporting an unknown reason #150
pumpAndSettle could have been a pump. Frames are labelled with their real number, and the stretches between recorded frames appear as a gap showing how many frames went by and how long they took on both clocks. Gaps hold nothing to select, so the arrow keys step straight over them. Also adds Timeline.renderedFrameCount and TimelineEvent.renderedFrameNumber.expect or an exception from the widget under test left the report ending at the last thing that worked. The event carries the real error message, a stack trace with the test framework folded out, a capture of the screen as the test left it, and the line that threw.act.dragUntilVisible() can now use any selector that resolves to a Scrollable as dragStart, so keyed or otherwise untyped scrollable selectors drag from that scrollable directly. #133 (thx @trejdych)spotAtPosition and WidgetSelector.atPosition to query widgets on the hit-test path for a global screen position. #28WidgetSelector.isPresent() and isAbsent() return bool without failing the test, and countWidgets() returns the number of matching widgets. Use them to branch test logic on the presence, absence or quantity of a widget. #30
if (spot<Tooltip>().withMessage('Open navigation menu').isPresent()) {
// ...
}
if (spot<Tooltip>().withMessage('Close menu').isAbsent()) {
// ...
}
final buttonCount = spot<ElevatedButton>().countWidgets();
final hasTwoButtons = spot<ElevatedButton>().countWidgets() == 2;
final hasAtLeastTwoButtons = spot<ElevatedButton>().countWidgets() >= 2;getDiagnosticProp<T>('name') is now also available on WidgetSelector, alongside the existing getWidgetProp, getElementProp, getStateProp and getRenderObjectProp readers. #30
final message = spot<Tooltip>().getDiagnosticProp<String>('message');WidgetSelector<AnyText>.whereIsEditable() and whereIsNotEditable() filter text matches by whether they come from an editable text input.
spotText('username').whereIsEditable().existsOnce();
spotText('Username').whereIsNotEditable().existsOnce();WidgetSnapshot.queryStats reports how much work the query engine performed to evaluate a selector, useful to debug slow queries. #148WidgetMatcher now always reports the widget of the frame it matched, not the current widget in the tree #159spot, spotKey, spotWidget, spotElement, spotTexts) no longer add a no-op WidgetTypeFilter<Widget> at the root.spotText, spotTextWhere, whereText, withText and hasText strip invisible characters (zero width space, soft hyphen, word joiner, BOM) and fold every Unicode space separator (Zs, e.g. non-breaking space) to a regular space. Meaningful characters (zero width joiner, bidi controls, the U+FFFC WidgetSpan placeholder) and line breaks are kept. #138 (thx @MichaelTamm)
spotText('foobar').existsOnce(); // matches Text('foo\u{200B}bar')
spotText('foo bar').existsOnce(); // matches Text('foo\u{00A0}bar')raw: true to spotText/spotTextWhere, or use whereRawText/withRawText/hasRawText on WidgetSelector<AnyText>. Also exposes AnyText.normalizeVisibleText, AnyText.extractText, and AnyTextContent (raw/normalized).spotText(text, exact: true) is now spotText(text, whole: true) — the flag controls whole-string vs. substring matching, not character handling. exact still works. #138ScreenshotAnnotator.cacheKey (default => null) allows caching of annotations #160ScreenshotAnnotator, which has already been a parameter of takeScreenshot(annotators: ...)loadAppFonts() now also registers a package's own fonts under packages/<self>/MyFont, so fonts referenced via package: '<self>' render instead of falling back to Ahem. #141Deprecated: file property. Still returns File but signature now returns dynamic for web support. Use createTempPngFile() or raw byte APIs instead
act.dragUntilVisible() now moves the target in the center of the viewport (one additional drag). parameter moveStep is now optional, default to half the scrollable size. The direction can be controlled with bool toStart.flutter test --platform chrome - don't generate the timeline HTML and screenshot pathsspot<GenericWidget>() can't find a widget because it is actually looking for GenericWidget<dynamic>build/timeline/<test_name>/screenshots/ for easier browser image resolution. Fixes issues with Firefox.act.tap when multiple or no widgets are foundexistsAtLeastNTimes(0) now reports a correct error messageChanges for WidgetSelector
.snapshotWidget(), .snapshotState(), .snapshotElement(), .snapshotRenderBox() and .snapshotRenderObject() now add a single consistent entry each to the timeline with consistent messages.Changes for WidgetSnapshot
discoveredRenderObjectdiscoveredRenderObjectsdiscoveredRenderBoxdiscoveredRenderBoxesremoveQuantityConstraints()Changes for class Screenshot (big breaking update!)
width, height, pixelRatio, namereadBytes(), readPngBytes(), readPngBytesSync() gives access to raw bytesfile property. Still returns File but signature now returns dynamic for web support. Use createTempPngFile() or raw byte APIs insteadcreateTempPngFile() writes the screenshot to a temporary file and returns the absolute file pathList<ScreenshotAnnotation> annotations, addAnnotation(), removeAnnotation() each layer is now separately availableflattenedImage() merges all layers into a single imageTimeline is now generated with Jaspr #76
act.tapAt() #80Timeline.addEvent() now returns the TimelineEventId idTimeline.updateEvent(id) and Timeline.removeEvent(id)off #88stateProp #93Add snapshotState<S>() final state = spot<MyContainer>().snapshotState<MyContainerState>()
snapshotState<S>()final state = spot<MyContainer>().snapshotState<MyContainerState>()snapshotRenderBox()WidgetPresence@useResult to .atMost(N), .atLeast(N), .amount(N) and .existsAtMostNTimes(N) to prevent missing assertionsexistsAtLeastNTimes dumping the widget tree to consoleTimelineMode.always/README.mdact to /README.mdsnapshotState<S>()
final state = spot<MyContainer>().snapshotState<MyContainerState>()snapshotRenderBox()WidgetPresence@useResult to .atMost(N), .atLeast(N), .amount(N) and .existsAtMostNTimes(N) to prevent missing assertionsexistsAtLeastNTimes dumping the widget tree to consoleTimelineMode.always/README.mdact to /README.mdDeprecate TimelineMode.record in favor of TimelineMode.reportOnError (which is the default) #68
loadAppFonts() to display your app fonts on screenshots #66loadFont() to load a fonts from a file. Useful when your app depends on preinstalled system fonts (loadFont('Comic Sans', [r'C:\Windows\Fonts\comic.ttf'])) #66WidgetSelector #71
spot<MyWidget>().getWidgetProp(widgetProp('color', (widget) => widget.color));spot<_MyContainer>().getStateProp(stateProp<String, _MyContainerState>('innerValue', (s) => s.innerValue));spot<_MyContainer>().getRenderObjectProp(renderObjectProp<Size, RenderBox>('size', (r) => r.size));getStateProp and stateProp to access state properties #71spot<_MyContainer>().existsOnce().getStateProp(stateProp('innerValue', (_MyContainerState s) => s.innerValue));timeline mode TimelineMode.always to always print a timeline after each test #68TimelineMode.record in favor of TimelineMode.reportOnError (which is the default) #68act.tap() now shows a Crosshair on the screenshotwhereWidgetProp(), whereElementProp() and whereRenderObjectProp() #67loadAppFonts() to display your app fonts on screenshots #66loadFont() to load a fonts from a file. Useful when your app depends on preinstalled system fonts (loadFont('Comic Sans', [r'C:\Windows\Fonts\comic.ttf'])) #66WidgetSelector #71
spot<MyWidget>().getWidgetProp(widgetProp('color', (widget) => widget.color));spot<_MyContainer>().getStateProp(stateProp<String, _MyContainerState>('innerValue', (s) => s.innerValue));spot<_MyContainer>().getRenderObjectProp(renderObjectProp<Size, RenderBox>('size', (r) => r.size));getStateProp and stateProp to access state properties #71
spot<_MyContainer>().existsOnce().getStateProp(stateProp('innerValue', (_MyContainerState s) => s.innerValue));timeline mode TimelineMode.always to always print a timeline after each test #68TimelineMode.record in favor of TimelineMode.reportOnError (which is the default) #68act.tap() now shows a Crosshair on the screenshotwhereWidgetProp(), whereElementProp() and whereRenderObjectProp() #67New: Timeline! Failing tests now print a timeline with screenshots of all interactions (actions and assertions) as HTML report #57
act.tap now checks for multiple tappable position when the center is not tappable for some reason #60act.tap now reports a useful error when the widget is 0px/0px or invisible #61masterAdd act.dragUntilVisible() #59
act.dragUntilVisible() #59Support for Flutter 3.22 Remove unused dependencies #55
Breaking Offstage support. By default Offstage widgets are not found by spot (). Use spotOffstage().spot () to find them. spotAllWidgets() returns ons
Offstage support. By default Offstage widgets are not found by spot<W>(). Use spotOffstage().spot<W>() to find them. spotAllWidgets() returns onstage and offstage widgets. Use .overrideWidgetPresence(WidgetPresence.offstage) to modify a WidgetSelector to search for offstage, onstage or combined #45act.enterText(spot<TextField>(), 'Hello World!') allows to enter text into a EditableText #51spot<ListView>().withParent(spot<Scaffold>().atMost(0))). It now throws to prevent unexpected behavior. #50act.tap(spot<ElevatedButton>()) now pumps automatically after the tap #52Remove deprecated property selector from withProp() and hasProp(). Use elementSelector instead
checks to 0.3.0 #48selector from withProp() and hasProp(). Use elementSelector insteadtest_api version range to include 0.7.XDeprecated: spotSingle () is now deprecated. Use spot () instead, or spot ().atMost(1) to indicate that only a single widget is expected.
spotText('dash') can now return multiple widgets.atLeast(n) and .atMost(n) and .amount(n) to force the number of expected widgets.
.atMost(0) can be used to test that a widget does not exist!spotSingle<W>() is now deprecated. Use spot<W>() instead, or spot<W>().atMost(1) to indicate that only a single widget is expected..first() and .last().atIndex(n) allows to get the widget at a specific index (when multiple are found)allWidgets in favor of spotAllWidgets() to avoid conflicts with local variablesgetDiagnosticProp<T>('name') for easy access to the values of a diagnostic property #40hasEffectiveTextStyle, withEffectiveTextStyleMatching(), withEffectiveTextStyle() #36, #38WidgetSelector.toString() has been improved, has now separators for stages and adds braces.
Example: Center with child SizedBox ❯ with parent (Scaffold ᗕ Row)Those changes can be breaking for packages that depend on spot or advanced usages, but should not affect most users.
WidgetSelector now has List<ElementFilter> stages, replacing the previous props, parents, children and elementFilters.WidgetSelector constructor and copyWith signature changed, reflecting the new properties.
createElementFilters(), createCandidateGenerator() and toStringWithoutParents() have been removed.WidgetSelector now has a quantityConstraint property (deprecates expectedQuantity) that allows setting the min and max number of expected widgets.WidgetSelector replaces SingleWidgetSelector and MultiWidgetSelector.doesNotExist() or .existsOnce() now return WidgetMatcher/MultiWidgetMatcher instead of WidgetSnapshot.
To get the WidgetSnapshot use snapshot() instead.WidgetSelector.cast because it lost information and was untestedPropFilter has been renamed to PredicateFilterPredicateWithDescription has been removedCandidateGenerator has been removedThis release contains breaking changes to the "internal" WidgetSelector API. Unless you are using the WidgetSelector directly, you should not be affec…
This release contains breaking changes to the "internal" WidgetSelector API.
Unless you are using the WidgetSelector directly, you should not be affected by this.
The end-user spot API is not affected.
WidgetSelector now has List<ElementFilter> stages, replacing the previous props, parents, children and elementFilters.WidgetSelector constructor and copyWith signature changed, reflecting the new properties. createElementFilters(), createCandidateGenerator() and toStringWithoutParents() have been removed.PropFilter has been renamed to PredicateFilterPredicateWithDescription has been removedCandidateGenerator has been removedWidgetSelector.toString() has been improved, has now separators for stages and adds braces. Example: Center with child SizedBox ❯ with parent (Scaffold ᗕ Row).atIndex(n) to be executed at the right time, not after all other filters.New getDiagnosticProp ('name') for easy access to the values of a diagnostic property #40
getDiagnosticProp<T>('name') for easy access to the values of a diagnostic property #40hasEffectiveTextStyle, withEffectiveTextStyleMatching(), withEffectiveTextStyle() #36, #38spotSingle () is now deprecated. Use spot () instead, or spot ().atMost(1) to indicate that only a single widget is expected.
Eventually Breaking, but only the class names. The end user API stays the same.
spotSingle<W>() is now deprecated. Use spot<W>() instead, or spot<W>().atMost(1) to indicate that only a single widget is expected.WidgetSelector replaces SingleWidgetSelector and MultiWidgetSelectorWidgetSelector now has a quantityConstraint property (deprecates expectedQuantity) that allows setting the min and max number of expected widgets..atIndex(n) allows to get the widget at a specific index (when multiple are found).first() and .last() now work after calling .copyWith().doesNotExist() or .existsOnce() now return WidgetMatcher/MultiWidgetMatcher instead of WidgetSnapshot. To get the WidgetSnapshot use snapshot() instead.spotText('a') can now return multiple widgetsWidgetSelector.cast because it lost information and was untestedNew prop API with hasWidgetProp() makes it easy to filter and assert properties of Widgets. This replaces the old hasProp() method which was based on
New prop API with hasWidgetProp() makes it easy to filter and assert properties of Widgets.
This replaces the old hasProp() method which was based on way to complicated package:checks context.
// Old ⛈️
spotSingle<Checkbox>().existsOnce().hasProp(
selector: (e) => e.context.nest(
() => ['Checkbox', 'value'],
(e) => Extracted.value((e.widget as Checkbox).value),
),
match: (it) => it.equals(true),
);
// New ✨
spotSingle<Checkbox>().existsOnce().hasWidgetProp(
prop: widgetProp('value', (widget) => widget.value),
match: (value) => value.isTrue(),
);
The prop API is also available for Element and RenderObject.
<summary>
<details>
├── Interface "NamedWidgetProp" added
├── Interface "NamedElementProp" added
├── Interface "NamedRenderObjectProp" added
├── Function "widgetProp" added
├── Function "elementProp" added
├── Function "renderObjectProp" added
├─┬ Class SelectorQueries
│ ├── Method "whereWidgetProp" added
│ ├── Method "whereElementProp" added
│ └── Method "whereRenderObjectProp" added
└─┬ Class WidgetMatcherExtensions
├── Method "getWidgetProp" added
├── Method "hasWidgetProp" added
├── Method "getElementProp" added
├── Method "hasElementProp" added
├── Method "getRenderObjectProp" added
└── Method "hasRenderObjectProp" added
</details> </summary>
Never miss asserting your WidgetSelector.
All methods returning a WidgetSelector are now annotated with @useResult.
This will cause a lint warning when you only define a WidgetSelector without asserting it.
spot<FloatingActionButton>().withChild(spotIcon(Icons.add)); // warning, no assertion
final plusFab = spot<FloatingActionButton>().withChild(spotIcon(Icons.add)); // ok, assigned
spot<FloatingActionButton>().withChild(spotIcon(Icons.add)).existsOnce(); // ok, asserted
It is now easy to directly access the Widget of a SingleWidgetSelector with snapshotWidget().
It also works for the associated Element and RenderObject. Use snapshotElement() and snapshotRenderObject().
-final checkbox = spotSingle<Checkbox>().snapshot().widget;
+final checkbox = spotSingle<Checkbox>().snapshotWidget();
print(checkbox.checkColor);
Add matchers .existsAtMostOnce() and .existsAtMostNTimes(x) #19
.existsAtMostOnce() and .existsAtMostNTimes(x) #19.withParent(parent)/.withParents([...]) #21.withChild(child)/.withChildren([...]) #21act.tap() now with any WidgetSelector that returns a single widget #23Deprecated: spotSingleText and spotTexts are deprecated in favor of spotText and the basic spot (), spot (), ... #18
act.tap is now async, use await act.tap() #17spotText('foo') finds any text on screen using "contains". The new AnyText widget combines Text, SelectableText, RichText and EditableText #18spotTextWhere((text) => ) allows to match text with custom logic #18spotSingleText and spotTexts are deprecated in favor of spotText and the basic spot<Text>(), spot<SelectableText>(), ... #18hasProp matcher can now check for null values with (it) => it.isNull() #18withDiagnosticProp now falls back to the default value of a DiagnosticNode #18Remove unused dependencies. Fixes incompatibility with latest test_api versions #55
Switch to renderView.size to get the window size
renderView.size to get the window sizeAdded screenshot methods #14 ```dart /// Takes a screenshot of the entire window await takeScreenshot();
/// Takes a screenshot of the entire window
await takeScreenshot();
/// Takes a screenshot of a single Screen/Widget
final homePage = spotSingle<HomePage>();
await takeScreenshot(selector: homePage);
/// Use it as extension
await spotSingle<HomePage>().takeScreenshot();
checks.dart which are required to use hasPropAdded act.tap(button) to tap widgets #9
act.tap(button) to tap widgets #9checks package #12SingleWidgetSnapshot.discoveredElements -> SingleWidgetSnapshot.discoveredElement #11Widen test_api range to support Flutter 3.22
Export all types from checks.dart which are required to use hasProp
checks.dart which are required to use hasPropFix compilation error with Flutter 3.0.0
spotTexts now matches EditableText and SelectableText #5
spotTexts now matches EditableText and SelectableText #5spotTexts now has generic type <W> instead of static Text. This changes the return type from MultiWidgetSelector<Text> -> MultiWidgetSelector<W> #5SingleWidgetSelector.withProp and MultiWidgetSelector.withProp.EditableText, ListTile, SelectableTextSupport for Flutter 3.0.0 / Dart 2.17
Fix WidgetSelector with parents that have parents #4
WidgetSelector with parents that have parents #4Allow defining WidgetSelector with children
WidgetSelector with childrenWidgetSelector with parentsFinder APIDiagnosticsNode)- Update package description - Add issue_tracker link - Add example folder
Extraction from wiredash repository
Your coding agent can read these notes before it upgrades. Set up the MCP server →