Spot
Flutter widget test toolkit - spot, act, validate. Better selectors, automatic screenshots, chainable.
Install / Use
/learn @passsy/SpotREADME
Spot
Spot is a toolkit for Flutter widget tests.<br/>
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)<br/>
⛓️ Chainable widget selectors<br/>
💙 Useful error messages (with full tree dump)<br/>
🌱 Opt-in, works with plain testWidgets()<br/>
💫 Compatibility with integration_test (except for the Timeline)<br/>
- Get started
- Timeline
- Screenshots
- spot - Widget selectors
- act - tap, drag, type
- Roadmap
- Project state
- License
Get started
flutter pub add dev:spot
1. Replace widget assertions (find) with spot.<br/>
2. Replace interactions like tester.tap() with act.tap() to interact with widgets.
With every call with spot or act, spot captures the current frame and adds it to the Timeline HTML report.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spot/spot.dart';
void main() {
testWidgets('existing Widget test', (tester) async {
await tester.pumpWidget(MyApp());
// await tester.tap(find.byType(ElevatedButton));
await act.tap(spot<ElevatedButton>());
// expect(find.text('monde'), findsOneWidget); // 🇫🇷
spot<Scaffold>().spotText('monde').existsOnce(); // 🇫🇷
// Automatically generates a timeline report on error
});
}
When your test fails, spot generates the Timeline HTML report with all assertions (spot) and gestures (act), automatic screenshots and more information.
Generating timeline report
View timeline here: file:///var/folders/0j/p0s0zrv91tgd33zrxb98c0440000gn/T/ecsTKx/existing-widget.html
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.
timeline.addEvent(
eventType: 'Received fake server response',
details: 'HTTP 200\n{"message": "Hello World"}',
color: Colors.orange,
screenshot: timeline.takeScreenshotSync(),
);
Change Timeline mode
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 recordedreportOnError(default): Only generate a Timeline report when a test failsalways: Always generate the Timeline report at the end of the testlive: Print all Timeline events to console as they happen
There are three ways to change the TimelineMode:
Single test
void main() {
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.
void main() {
globalTimelineMode = TimelineMode.off;
testWidgets('my widget test', (tester) async {/* ... */});
testWidgets('another test', (tester) async {
// overwrites the global mode
timeline.mode = TimelineMode.always;
// console: View timeline here: file:///var/folders/0j/p0s0zrv91t...
});
}
Global
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.
# Github Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
with:
channel: stable
- name: Run tests
run: flutter test --dart-define=SPOT_TIMELINE_MODE=off
Screenshots
Manual Screenshots
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';
void main() {
testWidgets('Take screenshots', (tester) async {
tester.pumpWidget(MyApp());
// Take a screenshot of the entire screen
await takeScreenshot();
// 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 widget
await spot<AppBar>().takeScreenshot();
await takeScreenshot(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.
void main() {
testWidgets('my widget test', (tester) async {
await loadAppFonts();
/* ... */
});
}
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>.
spot<AppBar>().spot<IconButton>();
spot<IconButton>(parents: [spot<AppBar>()]);
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
