Skip to content
FlightStack Docs
Sign in Start free

ScreenStack integration

ScreenStack is VooStack’s visual-regression product. The FlightStack integration captures Flutter test screenshots in CI, diffs them against your baseline branch, and (optionally) pushes the approved set to App Store Connect and Google Play.

  • Pixel diffs on every PR. Side-by-side view of changed widgets, light vs. dark, every device profile you configure.
  • Approval gates. Pull requests can be blocked until visual changes are explicitly approved.
  • Store sync. When you’re ready to ship, push the approved set directly to App Store Connect and Play Console — no manual screenshot wrangling.
  1. Create a ScreenStack project and link it to your FlightStack org.
  2. Instrument your tests with the screenstack Flutter package.
  3. Add the ScreenStack job to your CI pipelines.
  4. (Optional) Add the Upload Store Screenshots job to your release pipelines.

In the FlightStack dashboard, Settings → Integrations → ScreenStack. Authorize the connection, pick the project you want this org to default to. You can override per pipeline.

test/screens/login_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:screenstack/screenstack.dart';
import 'package:my_app/login_page.dart';
void main() {
testWidgets('Login — empty', (tester) async {
await tester.pumpWidget(const MyApp(home: LoginPage()));
await ScreenStack.capture(tester, 'login/empty');
});
testWidgets('Login — error', (tester) async {
await tester.pumpWidget(const MyApp(home: LoginPage(error: 'bad creds')));
await ScreenStack.capture(tester, 'login/error');
});
}

ScreenStack.capture() renders the current widget tree at the configured device profile + theme variant and queues it for upload. The actual upload happens through the job, so unit tests still run offline.

┌──────────┐ ┌────────┐ ┌──────────────┐
│ On PR │───▶│ Tests │───▶│ Screen Stack │
└──────────┘ └────────┘ └──────────────┘
┌──────────────┐
│ Notification │
│ (Slack PR) │
└──────────────┘

See the ScreenStack job for inputs.

┌────────────┐ ┌────────────┐ ┌──────────────┐ ┌────────────────────┐
│ On Tag │───▶│ Build iOS │───▶│ Screen Stack │───▶│ Approval │
│ v* │ │ │ │ │ │ "ship screenshots?"│
└────────────┘ └────────────┘ └──────────────┘ └────────────────────┘
┌────────────────────┐
│ Upload Store │
│ Screenshots │
└────────────────────┘

See Upload Store Screenshots.

FamilyProfileUse for
iPhone 6.7”iphone-15-pro-maxRequired for App Store submissions
iPhone 6.5”iphone-15-proRequired for older devices
iPad 13”ipad-13Required if you target tablet
Pixelpixel-7Required for Play Store submissions
Android tabletnexus-tabletRequired if you target Android tablet

Marketing-quality screenshots usually need a separate, polished pipeline run with mocked data and explicit pumpAndSettle() calls. Don’t ship debug-mode screenshots to the App Store.

This is the one that bites. If any route in your config requires a signed-in user, you must provide your own capture test. Without it the agent generates a stub that pumps your app with no credentials, every protected route redirects to your sign-in page, and you capture the same screen once per canvas. The job used to report success while doing it.

Create integration_test/screenstack_test.dart. The agent looks for that exact path first and only falls back to the stub when it is missing.

integration_test/screenstack_test.dart
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:screenstack_sdk/screenstack_sdk.dart';
import 'package:your_app/screenstack_config.dart';
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Capture ScreenStack screenshots', (tester) async {
// Read from the process environment. Pipeline variables arrive as real
// environment variables, NOT as --dart-define, so String.fromEnvironment
// reads back empty here.
final token = Platform.environment['SCREENSTACK_ACCESS_TOKEN'] ?? '';
if (token.isEmpty) {
fail('Set SCREENSTACK_ACCESS_TOKEN on the screen-stack job.');
}
// Everything your main() does before runApp. The SDK pumps a bare app
// widget, so config and DI have to be in place first.
await AppConfig.initialize(baseUrl: 'https://api.example.com/');
await captureScreenStackScreenshots(
tester,
binding,
screenstackConfig,
setup: (tester, context) async {
await signInWithToken(token);
await tester.pumpAndSettle();
// Assert you actually left the sign-in page before capturing.
},
);
});
}

Three things to get right:

  1. Bootstrap first. captureScreenStackScreenshots pumps config.appBuilder() itself. Anything your main() normally does before runApp has to happen before you call it.
  2. Read credentials from Platform.environment. Not String.fromEnvironment.
  3. Fail loudly when credentials are missing or stale. A green job that published the wrong artwork is worse than a red one.

Store the token as a pipeline variable on the screen-stack job, not in the job config.

Every screenshot is the sign-in screen Your screens need auth and you have no integration_test/screenstack_test.dart. See Screens behind a login. FlightStack blocks the upload when all captured screenshots are byte-identical, so the listing keeps its previous artwork.

Captures missing on the dashboard Check the agent logs for screenstack/upload failures. The most common cause is an expired ScreenStack project token — re-authorize the integration in dashboard settings.

Random diffs from font rendering Lock the test font with --dart-define=FONT=NotoSans or use Flutter’s loadAppFonts() in your test harness — system fonts drift between CI agents.