App Screenshots
Documentsapp-screenshotsApp Screenshots is an Agent Skill for documents published in the matthiasn/lotti repository on GitHub: a SKILL.md instruction file an AI agent can load to do this task.
Capture in-app screenshots of a widget or flow at mobile + desktop sizes — offline, deterministic, real fonts and icons — using the reusable screenshot harness. Use when asked to "show me X in the app", "screenshot this screen", or to preview a UI change for review on phone vs desktop.
How to use this skill
Bring this guide into your coding agent with a prompt tailored to the tool you use.
- Open your project in Codex.
- Copy and paste the prompt below.
- Review files and risks before approving any action.
Inspect the Agent Skill at https://github.com/matthiasn/lotti/blob/HEAD/.claude/skills/app-screenshots/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/app-screenshots, but wait for my approval before writing files or running scripts.
Copying this prompt does not install or run the skill. Review third-party files before use.
In-App Screenshots
Produces PNGs of a widget or flow rendered inside the real app shell, at phone
and desktop viewports, with real fonts and icons. It runs through
flutter test --update-goldens — fully offline and deterministic — so it does
not require a display, the GUI app, or driving the device.
This is a visualization tool, not a golden test: there are no committed reference images. You write a throwaway capture test, emit the PNGs, share them, then delete the test. Nothing screenshot-related gets committed unless the user explicitly asks to keep it.
When to use
- "Show me the <screen/modal/dialog> in app context."
- "What does this change look like on mobile vs desktop?"
- Previewing a UI change for design review.
For "is this change actually wired up / does it behave correctly", prefer the
verify skill (runs the real app). This skill is about appearance.
The harness
test/test_utils/screenshot_harness.dart provides:
loadAppFonts()— loads every bundled font (app families + Material/Cupertino icon fonts + icon-font packages) fromFontManifest.json. Call once insetUpAll. Without it, glyphs and icons render as boxes.screenshotTheme({bool dark = true})— the productionwithOverridestheme withInterapplied to the Material text themes (the bare test theme falls back to an unbundled font otherwise).captureInApp(tester, {child, name, size, dark, devicePixelRatio, overrides, interaction, outputDir})— sizes the view, pumpschildin the app shell, runsinteraction(open a modal, tap a FAB, focus a field…), then writes<outputDir>/<name>.pngrelative to the calling test file's directory.ScreenshotViewport.phone(390×844) andScreenshotViewport.desktop(1280×800).
Workflow
-
Find the surface and how to reach the state. Identify the entry widget (often a tab/page like
ProjectsTabPage) and the tap/scroll needed to reach the state to capture (e.g. tapping the FAB to open a modal). Reuse existing widget tests for that surface to learn the required service registrations and provider overrides — copy theirsetUp. -
Write a throwaway capture test at
test/_scratch_capture_test.dart(the_scratch_prefix marks it disposable). UsesetUpTestGetIt+additionalSetupfor services the surface resolves throughgetIt, and pass provider overrides viacaptureInApp(overrides: ...). Drive the state ininteraction. -
Generate the PNGs:
fvm flutter test --update-goldens test/_scratch_capture_test.dartImages land in
test/screenshots/(theoutputDir, next to the test). -
View and share. Read the PNGs to inspect them; send them to the user with
SendUserFile. -
Clean up — this is mandatory unless the user asks to keep anything:
rm test/_scratch_capture_test.dart && rm -rf test/screenshots
Example capture test
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/misc.dart'; // Override
import 'package:flutter_test/flutter_test.dart';
import 'package:lotti/classes/project_data.dart';
import 'package:lotti/features/projects/model/projects_overview_models.dart';
import 'package:lotti/features/projects/state/project_providers.dart';
import 'package:lotti/features/projects/ui/pages/projects_tab_page.dart';
import 'package:lotti/features/user_activity/state/user_activity_service.dart';
import 'package:lotti/get_it.dart';
import 'package:lotti/services/entities_cache_service.dart';
import 'package:lotti/services/nav_service.dart';
import 'package:mocktail/mocktail.dart';
import 'features/categories/test_utils.dart';
import 'features/projects/test_utils.dart';
import 'mocks/mocks.dart';
import 'test_utils/screenshot_harness.dart';
import 'widget_test_utils.dart';
void main() {
setUpAll(loadAppFonts);
final groups = [
ProjectCategoryGroup(
categoryId: 'lotti',
category: CategoryTestUtils.createTestCategory(id: 'lotti', name: 'Lotti'),
projects: [
ProjectListItemData(
project: makeTestProject(id: 'p1', title: 'Daily Operating System'),
category: CategoryTestUtils.createTestCategory(id: 'lotti', name: 'Lotti'),
taskRollup: const ProjectTaskRollupData(totalTaskCount: 25),
),
],
),
];
List<Override> overrides() => [
projectsOverviewProvider.overrideWith(
(ref) => Stream.value(ProjectsOverviewSnapshot(groups: groups)),
),
visibleProjectGroupsProvider.overrideWith((ref) => AsyncValue.data(groups)),
];
setUp(() async {
await setUpTestGetIt(
additionalSetup: () {
final activity = MockUserActivityService();
when(activity.updateActivity).thenReturn(null);
getIt.registerSingleton<UserActivityService>(activity);
final nav = MockNavService();
when(() => nav.isDesktopMode).thenReturn(false);
when(() => nav.desktopSelectedProjectId)
.thenReturn(ValueNotifier<String?>(null));
getIt.registerSingleton<NavService>(nav);
final cache = MockEntitiesCacheService();
when(() => cache.getCategoryById(any())).thenReturn(null);
getIt.registerSingleton<EntitiesCacheService>(cache);
},
);
});
tearDown(tearDownTestGetIt);
Future<void> openCreate(WidgetTester tester) =>
tester.tap(find.bySemanticsLabel('New Project'));
testWidgets('phone', (tester) async {
await captureInApp(
tester,
child: const ProjectsTabPage(),
name: 'create_mobile',
overrides: overrides(),
interaction: openCreate,
);
});
testWidgets('desktop', (tester) async {
await captureInApp(
tester,
child: const ProjectsTabPage(),
name: 'create_desktop',
size: ScreenshotViewport.desktop,
overrides: overrides(),
interaction: openCreate,
);
});
}
Gotchas
Overridelives inpackage:flutter_riverpod/misc.dartin this Riverpod version — import it for theList<Override>type (separate fromflutter_riverpod.dart, which hasAsyncValue).- Register every service the surface touches via
setUpTestGetIt'sadditionalSetup, or the widget throws on a missinggetItregistration. Modals pull in their own deps (e.g. the create modal'sCategoryFieldneedsEntitiesCacheService). - Desktop layout is width-driven.
captureInApp(size: ScreenshotViewport.desktop)sets both the view physical size andMediaQuery, so width-gated split views (isDesktopLayout) and the dialog branch (modal page breakpoint) engage correctly. - Capture target is
find.byType(MaterialApp)so overlay content (modals, dialogs, toasts) is included, not just the page body. - Don't commit the scratch test or
test/screenshots/. Delete them when done. The harness itself is the only permanent artifact.