Use when writing or fixing unit/widget tests for Flutter code that uses cached_video_thumbnail - tests that must run hermetically under flutter test without native plugins, platform channels, or network, or that hit MissingPluginException / path_provider errors from ThumbnailEngine or VideoThumbnailImage.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cached-video-thumbnail:mock-thumbnail-engineThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The package has one designed test seam, and it makes everything else
The package has one designed test seam, and it makes everything else
unnecessary: ThumbnailEngine.forTesting(extractor:, directory:) builds a
fully real engine (scheduler, disk cache, metrics) on top of an injected
ThumbnailExtractor fake and a temp directory. The only thing faked is
the native decode. Never mock the Pigeon channel, path_provider, or the
engine class itself.
ThumbnailEngine.instance has no setter, and a widget that hardcodes
VideoThumbnailImage(source) resolves through the real singleton
(-> MissingPluginException under flutter test). Do NOT work around that
with ImageCache seeding tricks; fix the seam - it is a one-parameter
change:
class MyVideoTile extends StatelessWidget {
const MyVideoTile({super.key, required this.videoUrl, this.engine});
final String videoUrl;
final ThumbnailEngine? engine; // test seam; null = production singleton
@override
Widget build(BuildContext context) => Image(
image: VideoThumbnailImage(
VideoSource.network(Uri.parse(videoUrl)),
spec: const ThumbnailSpec(maxWidth: 320, maxHeight: 320),
engine: engine, // public named parameter, exists for exactly this
),
fit: BoxFit.cover,
);
}
Note: VideoThumbnailImage's engine: parameter is declared as a Dart
private named parameter (this._engine) - it does not appear in some API
listings, but callers pass it as engine:. Non-widget code should take a
ThumbnailEngine engine constructor/function parameter the same way.
Take fake_thumbnail_extractor.dart
into the app's test/support/. Contract it honors (required of any fake):
write real decodable image bytes into destPath (the engine commits that
file into its cache and the provider really decodes it), return the true
dimensions, throw ThumbnailException for failure cases - never return
without producing the file.
late Directory root;
late FakeThumbnailExtractor extractor;
late ThumbnailEngine engine;
setUp(() async {
root = await Directory.systemTemp.createTemp('thumb_test');
extractor = FakeThumbnailExtractor();
engine = ThumbnailEngine.forTesting(
extractor: extractor,
directory: Directory('${root.path}/cache'),
);
PaintingBinding.instance.imageCache..clear()..clearLiveImages();
});
tearDown(() => root.delete(recursive: true));
forTesting is @visibleForTesting: the analyzer allows it in test/
and integration_test/ directories; if it warns elsewhere, the call is in
the wrong place. Each test gets a fresh engine + directory - engines are
cheap and isolated; never share cache dirs between tests.
Engine-level (plain test, no pumping needed):
final thumb = await engine.getThumbnail(
VideoSource.asset('assets/v.mp4'),
spec: const ThumbnailSpec(maxWidth: 64, maxHeight: 64),
);
expect(thumb.wasCached, isFalse);
expect(extractor.callCount, 1);
// Second call: cache hit, no new extraction.
Widget-level (testWidgets): wrap the pipeline in tester.runAsync -
real file IO and image decoding never complete inside the fake-async test
zone:
await tester.runAsync(() async {
await tester.pumpWidget(MaterialApp(
home: MyVideoTile(videoUrl: url, engine: engine),
));
// Let extraction + decode land; then settle frames.
await Future<void>.delayed(const Duration(milliseconds: 50));
});
await tester.pump();
final raw = tester.widget<RawImage>(find.byType(RawImage));
expect(raw.image?.width, 2); // the fake's 2x2 PNG
Behavior cases the fake supports: failWith = ThumbnailException(...) to
drive errorBuilder paths; gated = true + release() to test loading
states and cancellation; delay for latency; cancelled list to assert
cancellation reached the extractor.
fakeAsync/FakeAsync does not play well with the engine's real
file IO - disk cache writes are genuine async IO that fake clocks do
not advance. Use real async (runAsync, pumpEventQueue); reserve
fake clocks for pure scheduler-timing units, as the package's own tests
do.metrics counters: prefer counters over
timing percentiles; percentiles are null until samples exist.ThumbnailEngine.instance in host tests at all; even
though construction is currently inert, only the injected engine is
guaranteed hermetic.If forTesting or the engine: parameter is missing in the installed
version, this skill predates or postdates that API - read the installed
package source in .pub-cache (lib/src/engine.dart,
lib/src/image_provider.dart) and adapt; say which version you found.
"CI fails with MissingPluginException in golden tests of our video feed."
-> add the engine seam to the feed widget, inject
ThumbnailEngine.forTesting with the bundled fake (instant 2x2 PNGs),
runAsync around pumps, goldens now deterministic and offline.
Guides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.
npx claudepluginhub omar-hanafy/cached_video_thumbnail --plugin cached-video-thumbnail