Orientação gerada a partir de informações públicas. Apenas para referência.
Spot é uma ferramenta de teste para Flutter projetada para simplificar os testes de widgets com sintaxe intuitiva e funcionalidades aprimoradas. Permite aos desenvolvedores 'encontrar' widgets usando seletores poderosos e legíveis, realizar ações com 'act' e validar estados com facilidade. O pacote suporta captura automática de telas para testes de regressão visual e oferece chamadas de métodos encadeáveis para código de teste mais limpo. Construído para testes multiplataforma, o Spot funciona perfeitamente em Android, iOS, macOS, Windows e Linux. Melhora a confiabilidade dos testes e a experiência do desenvolvedor, reduzindo o código repetitivo e aumentando a clareza nas afirmações de teste.
Casos de uso
Teste de interação de widget
Testes de regressão visual
Validação de interface do usuário multiplataforma
Relatórios de teste automatizados
Configuração de teste simplificada
Teste de fluxo de widget ponto a ponto
Principais recursos
Sintaxe de seletor melhorada
Captura automática de tela
API encadeável
Melhoria na legibilidade dos testes
Suporte multiplataforma
Reduzido boilerplate
Indicado para
Desenvolvedores Flutter escrevendo testes de widget
Equipes que priorizam a manutenibilidade dos testes
Projetos que exigem verificações de regressão visual
Pipelines de CI/CD com testes automatizados
Desenvolvedores procurando código de teste mais limpo
Considerações
Requer integração com o framework de teste existente
Pode aumentar o tempo de execução dos testes devido às capturas de tela
Limitado a testes de nível de widget
Depende da infraestrutura de teste do Flutter
Não adequado para testes unitários ou de integração fora da camada de interface do usuário
README original
Snapshot do README original do repositório-fonte vinculado. Consulte o repositório para o conteúdo mais recente.
Spot
Spot is a toolkit for Flutter widget tests.
It simplifies queries and assertions against the widget tree (better finder API called spot) and
visualizes the steps of a widget test as HTML report with automatic screenshots, the Timeline.
🖼️ Automatic screenshots during widget tests (Timeline)
⛓️ Chainable widget selectors
💙 Useful error messages (with full tree dump)
🌱 Opt-in, works with plain testWidgets()
💫 Compatibility with integration_test (except for the Timeline)
When your test fails, spot generates the Timeline HTML report with all assertions (spot) and gestures (act), automatic screenshots and more information.
You can open the local Timeline report in your browser.
Timeline
Overview
The Timeline is a visual representation of the widget test run, rendered as an HTML report.
It shows all interactions with the spot package, like spot and act.
The focus on screenshots with annotations makes it easy to follow what happened during the test run.
At any point in the timeline, it is possible to jump back into the test code.
The Timeline is constructed during a widget test with the first interaction with spot.
The more frames of a test are asserted with spot, the more detailed the Timeline becomes.
By default, the Timeline is automatically generated when a test fails.
The path to the HTML file is printed to the console.
Successful tests skip the Timeline generation (and extra work).
Widget tests without any call to spot do not generate a Timeline.
Add custom events to the Timeline
You can add custom events to the Timeline to better understand what is happening in your test.
The timeline API is completely open and allows adding any event you want.
Spot automatically generates a Timeline HTML report when a test fails.
Change this behavior by adjusting the TimelineMode, e.g. during development, to always generate the timeline or skip it for parts of a test.
TimelineMode defines the following values:
off: No events will be recorded
reportOnError(default): Only generate a Timeline report when a test fails
always: Always generate the Timeline report at the end of the test
live: Print all Timeline events to console as they happen
There are three ways to change the TimelineMode:
Single test
voidmain() {
testWidgets('my widget test', (tester) async {
timeline.mode = TimelineMode.always;
/* ... */
});
testWidgets('complex test', (tester) async {
timeline.mode = TimelineMode.off;
/* a long setup which should not be recorded */
timeline.mode = TimelineMode.reportOnError;
// relevant test code
});
}
Entire file
Changing the globalTimelineMode only a default at the beginning of each test.
It can be changed by each test individually.
flutter test --dart-define=SPOT_TIMELINE_MODE=always
Timeline in console on CI
On CI servers, it might be hard to access the HTML reports.
The only output is often the console output.
Unless the reports are explicitly archived after a run, they are usually inaccessible.
Spot automatically detects CI systems and dumps the Timeline to the console when a test fails.
That might be ugly to read, but all information is better than none.
To disable this behavior, set SPOT_TIMELINE_MODE=off as an environment variable.
The Timeline automatically captures screenshots. But those are always for the entire screen and are not available during the test itself.
Use await takeScreenshot() to get the current pixels on the virtual screen.
takeScreenshot also takes a selector parameter to screenshot a single widget. This is useful to check the actual rasterized image (pixels) of a widget.
import'package:flutter/material.dart';
import'package:flutter_test/flutter_test.dart';
import'package:spot/spot.dart';
voidmain() {
testWidgets('Take screenshots', (tester) async {
tester.pumpWidget(MyApp());
// Take a screenshot of the entire screenawaittakeScreenshot();
// console:// Screenshot file:///var/folders/0j/p0s0zrv91tgd33zrxb88c0440000gn/T/spot/screenshot_test:10-s83dv.png// taken at main.<fn> file:///Users/pascalwelsch/Projects/passsy/spot/test/spot/screenshot_test.dart:10:10// Take a screenshot of a single widgetawait spot<AppBar>().takeScreenshot();
awaittakeScreenshot(selector: spot<AppBar>());
// console:// Screenshot file:///var/folders/0j/p0s0zrv91tgd33zrxb88c0440000gn/T/spot/screenshot_test:16-w8UPv.png// taken at main.<fn> file:///Users/pascalwelsch/Projects/passsy/spot/test/spot/screenshot_test.dart:16:24
});
}
Load Fonts
The Timeline shows a rich report of the significant events during the test with screenshots.
To better understand what's shown on the screenshots, it's important to load the fonts from your app, otherwise Flutter renders the text with the unreadable Ahem default font.
Use await loadAppFonts() to load the fonts defined in the pubspec.yaml.
Additionally, loadAppFonts() loads the Roboto font, which is the default font in Flutter tests.
Before:
After:
Widget selectors spot
Chain selectors
You know exactly where your widgets are.
Like a button in the AppBar or a Text in a Dialog.
Spot allows you to chain matchers, narrowing down the search space.
Chaining allows spot to create better error messages for you.
Spot follows the chain of your selectors and can tell you exactly where the widget is missing.
Like: Could not find "IconButton" in "AppBar", but found these widgets instead: <AppBar-widget-tree>.
Both syntax are identical. The first is shorter for when you only need a single parent.
The second allows checking for multiple parents, which is only required for rare use cases.
Selectors
Spot has two features, creating selectors and asserting on them with matchers.
A selector is a query to find a set of widgets.
Like a SQL query, or a CSS selector.
It is only a description of what to search for, without actually doing the search.
Selectors can be rather complex, it is therefore recommended to reuse them.
You can even save them top-level and reuse them across multiple tests.
A WidgetSelector may return 0, 1 or N widgets.
Depending on how many widgets you expect to find, you should use the corresponding matchers.
Selector entry points
Use spot<W>() when you know the widget type.
Use the specialized entry points when the thing that identifies a widget is not only its type.
spot<ElevatedButton>(); // widgets by exact runtime typespotText('Save'); // Text, SelectableText and EditableTextspotTextWhere((it) => it.startsWith('Save')); // custom text checksspotIcon(Icons.save); // Icon widgets by IconDataspotKey(constValueKey('save')); // widgets by KeyspotWidget(myButton); // widgets by identityspotElement(element); // the widget associated with an ElementspotAllWidgets(); // onstage and offstage widgetsspotOffstage(); // only offstage widgets
Selectors also interoperate with Flutter's Finder API.
This is useful when migrating existing tests one assertion at a time.
spotText() is the easiest way to find text on screen.
It matches Text, SelectableText and EditableText.
By default, it matches the text a user sees, so invisible characters are removed and Unicode space separators are treated like regular spaces.
This keeps tests readable when Flutter inserts zero width spaces or localized content uses non-breaking spaces.
whereIsEditable() keeps text from enabled inputs that accept user edits.
whereIsNotEditable() keeps regular display text and disabled or read-only inputs.
Use raw: true when exact characters matter.
Use whereRawText, withRawText and hasRawText for exact-character selector and matcher chains.
Use whole: true for whole-string matching.
Use raw: true for exact-character matching.
The deprecated exact flag meant whole-string matching, not exact-character matching.
Use AnyText.normalizeVisibleText() and AnyText.extractText() when building custom text tooling.
AnyText.extractText() returns AnyTextContent with both the exact raw text and the normalized text that spotText() matches by default.
Use spotAtPosition() to query the widgets on the hit-test path at a global screen position.
Use .atPosition() to filter an existing selector by the same hit-test path.
Flutter hit testing decides which widgets match.
Widgets that visually overlap the coordinate but do not receive hit-test events are not included.
Better errors
By chaining widget selectors, spot can provide better errors by searching the parent scope first for potential candidates.
This can save a lot of time when debugging failing widget tests.
Here, the settings icon could not be found in the AppBar.
Classic widget tests would show the following error using findsOneWidget.
expect(find.byIcon(Icons.settings), findsOneWidget);
>>> Expected: exactly one matching node in the widget tree
>>> Actual: _WidgetIconFinder:<zero widgets with icon "IconData(U+0E57F)" (ignoring offstage widgets)>
>>> Which: means none were found but one was expected
The error message above is not really helpful, because the actual error is not that there's no icon, but the Icons.home instead of Icons.settings.
The spot error message is much more helpful, showing two potential candidates in the AppBar.
Could not find AppBar ᗕ Icon Widget with icon: "IconData(U+0E57F)" in widget tree, expected exactly
1
A less specific search (Icon with parent AppBar) discovered 2 matches!
View timeline here: file:///var/folders/0j/p0s0zrv91tgd33zrxb88c0440000gn/T/hDEgVS/timeline-narrow-down-search-down-the-tree.html
Spot was able to find two Icon Widgets in the AppBar (with the wrong icon). They are presented in the Timeline report, highlighted in the screenshot.
A picture is worth a thousand lines of code.
Complex Example
import'package:flutter/material.dart';
import'package:flutter_test/flutter_test.dart';
import'package:spot/spot.dart';
voidmain() {
testWidgets('Widget test with spot', (tester) async {
// Create widget selectors for elements in the widget treefinal scaffold = spot<MaterialApp>().spot<Scaffold>();
final appBar = scaffold.spot<AppBar>();
// Assert for values of widgets
appBar.spotText('Dash').hasFontSize(14).hasFontColor(Colors.black87);
// Find widgets based on child widgets
appBar
.spot<IconButton>(children: [spotIcon(Icons.home)])
.existsOnce()
.hasTooltip('home');
// Find widgets based on multiple parent widgets
spot<Icon>(parents: [appBar, spot<IconButton>()])
.existsExactlyNTimes(2)
.all((icon) {
icon.hasColorWhere((color) => color.equals(Colors.black));
});
// Interact with widgets using `act`final button = spot<FloatingActionButton>();
await act.tap(button);
final text = spot<TextField>();
await act.enterText(text, 'Hello World');
// at the end of a failed test, spot will generate a Timeline HTML file
});
}
Matchers
After creating a selector, you want to assert the widgets it found.
The snapshot() method creates a WidgetSnapshot of the widget tree at that point in time and finds all widgets that match the selector.
Quantity matchers
The easiest matchers are the quantity matchers. They allow checking how many widgets were found.
existsOnce() asserts that exactly one widget was found
doesNotExist() asserts that no widget was found
existsExactlyNTimes(n) asserts that exactly n widgets were found
existsAtLeastOnce() asserts that at least one widget was found
existsAtMostOnce() asserts that at most one widget was found
final selector = spot<ElevatedButton>();
// calls snapshot() internallyfinal matchOne = selector.existsOnce();
final matchMultiple = selector.existsExactlyNTimes(5);
selector.doesNotExist(); // end, nothing to match on
To check the presence or absence of a widget without failing the test, use isPresent() or isAbsent().
They return a bool instead of throwing, allowing test logic to branch on the presence or absence of a widget.
Quantity constraints are ignored because these methods only check whether at least one widget matches the selector.
Use countWidgets() to read the exact number of matching widgets.
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;
Property matchers
The property matchers allow asserting on the properties of the widgets.
You don't have to use expect(), instead you can use the has*/is* matchers directly.
spot<Tooltip>()
.existsOnce() // takes snapshot and asserts quantity// start your chain of matchers
.hasMessage('Favorite')
.hasShowDurationWhere(
(it) => it.isGreaterOrEqual(Duration(seconds: 1000)),
)
.hasTriggerMode(TooltipTriggerMode.longPress);
Instead of asserting, read the raw property values to pass them into other functions.
Each has* matcher has a get* counterpart on WidgetMatcher.
final message = spot<Tooltip>().existsOnce().getMessage();
Read any property with getWidgetProp, getElementProp, getStateProp, getRenderObjectProp or getDiagnosticProp, directly on the WidgetSelector or on a WidgetMatcher.
final size = spot<Tooltip>().getRenderObjectProp(
renderObjectProp<Size, RenderBox>('size', (r) => r.size),
);
final message = spot<Tooltip>().getDiagnosticProp<String>('message');
Debug slow selectors
Call snapshot() when you need to inspect the widgets a selector found.
The returned WidgetSnapshot exposes the matching widgets, elements, render objects and query statistics.
final snapshot = spot<AppBar>().spotText('Home').snapshot();
print(snapshot.discoveredWidgets);
print(snapshot.discoveredElements);
print(snapshot.queryStats);
WidgetSnapshot.queryStats reports how much work the query engine performed.
Use it to debug selector caching, compare performance before and after a selector change, or catch bugs where relative selectors make query work grow exponentially.
The exact numbers are implementation details and can change between spot versions.
It is recommended to use matchers instead of selectors once you have narrowed down the search space to the widget you want to assert on.
This makes the error messages much clearer.
Instead of widget not found you'll get Found ToolTip with message 'Settings' but expected 'Favorite' as error message.
To interact with widgets, use the act API.
The collection of functions mimics user interactions with the UI.
It tries to reach feature parity with the WidgetTester API, while drastically improving common pitfalls and error messages.
tap
Triggers a tap event (down + up) on the found widget.
Tapping a widget looks almost identical to the WidgetTester API but with a few improvements.
Checks that the widget is within the window bounds
When partially visible, automatically taps the visible part (and prints a warning when <80% tappable)
When fully covered, shows which widgets overlay it
Adds screenshot with crosshair annotation to the Timeline
pumps automatically after the tap
When multiple widgets are found, it prints a useful error message
inspectTap
Answers whether a widget can be tapped, and why not, without tapping it.
final inspection = act.inspectTap(spot<SubmitButton>());
expect(inspection.canTap, isFalse);
act.inspectTap() sends no pointer events and pumps no frame, so the app is in the same state afterwards.
That makes it the way to assert that a widget is not tappable, because act.tap() proves the same thing by failing the test.
inspection.tapFailure is null when the widget can be tapped, otherwise it carries the reason, with the widgets that explain it.
The typed getters fail the test with the full diagnostics when the widget turned out to be untappable for a different reason than expected:
final reason = act.inspectTap(spot<SubmitButton>()).tapFailure!.tapCoveredReason;
expect(reason.primaryCover?.widget, isA<ColoredBox>());
There is one reason per way a tap can fail: TapNotFoundReason, TapMultipleWidgetsFoundReason, TapNoRenderObjectReason, TapNonRenderBoxReason, TapOutsideViewportReason, TapAbsorbedReason, TapIgnoredReason, TapOffstageReason, TapZeroSizeReason, TapCoveredReason and TapUnknownReason.
A widget that can be tapped may still be partially covered, which no failure can describe.
inspection.samples answers that for tappable and untappable widgets alike:
final samples = act.inspectTap(spot<SubmitButton>()).samples!;
print('${samples.hittablePercent}% of the button reacts to taps');
for (final blocker in samples.blockers) {
print(blocker); // ColoredBox (46%)
}
act.tap() throws a TapFailure carrying the same inspection, so the reason of a real tap can be asserted the same way:
The act.enterText() automatically searches for the first EditableText widget within the provide selector (spot<EmailAddress>()).
It then enters the text in one go (like pasting it) and pumps the widget tree.
The act.dragUntilVisible() continuously drags from the center of dragStart until it reaches dragTarget.
When dragStart resolves to a Scrollable, act.dragUntilVisible() drags from that Scrollable directly.
Otherwise, it uses the closest Scrollable ancestor of dragStart.
The direction is determined by the moveStep parameter.
Scrolling towards the end (bottom) of a list is archived with a negative dy (Offset(0, -100)).
scrolling towards the beginning (top) requires a positive dy (Offset(0, 100)).
Scroll faster by increasing the moveStep value. Make sure to keep it below the height of the viewport or the dragTarget might be missed.
Check the Timeline for screenshots of the dragged area. It will help to determine good values for moveStep, minimizing the number of drags.
Increase maxIteration (default 30) for very long lists if the item is not found within that many drags.
more act functions
Please create an issues when you miss a function or have a suggestion for a new one. Any contribution in this direction is welcome.
Roadmap
✅ Make chainable WidgetSelectors
✅ Print full widget tree when assertions fail
✅ Allow defining WidgetSelector with children
✅ Allow defining WidgetSelector with parents
✅ Interop with Finder API
✅ Match properties of widgets (via DiagnosticsNode)
✅ Allow matching of nested properties (with checks API)
✅ Generate code for custom properties for Flutter widgets
✅ Allow generating code for properties of 3rd party widgets
✅ Interact with widgets (act)
✅ Allow manually printing a screenshot at certain points
✅ Negate child matchers
✅ Simplify WidgetSelector API
✅ Create screenshot when test fails
✅️ Create interactive HTML page with all widgets and matchers when test fails
✅ Automatically create report with screenshots of all user interactions
✅ loadAppFonts()
⬜️ More act features, feature parity with WidgetTester
⬜️ Combine multiple WidgetSelectors with and
⬜️ Become the de facto Widget selector API for patrol
⬜️ Single pixel color testing
⬜️ pumpSmart()
Project state
100% production ready.
The WidgetSelector API is stable
The existing act API is limited, but what exists is great.
The Timeline is in its early stages but will be improved over time.
License
Copyright 2022 Pascal Welsch
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.