From c1c72da7f54804a64a9fa418c3d8e2ac1bd8a8ff Mon Sep 17 00:00:00 2001 From: Koji Wakamiya Date: Wed, 23 Sep 2026 13:58:08 +0900 Subject: [PATCH] test(e2e): give each case of a multi-case test its own test Five previews (throwing, resolved, shadowing, crashing, unpainted) were one test, as were the three --capture screen cases and the two --enter cases. A failure stopped the cases after it, and lastRun could read the run of an earlier case. Each case is now a test with its own copy of the example; the annotation cases move to annotations_test.dart. Co-Authored-By: Claude Opus 5.5 (1M context) --- test/e2e/annotations_test.dart | 100 +++++++++++++++++++++++++++++++++ test/e2e/capture_test.dart | 78 ++++++++++++++----------- test/e2e/enter_test.dart | 8 ++- test/e2e/errors_test.dart | 82 ++------------------------- 4 files changed, 156 insertions(+), 112 deletions(-) create mode 100644 test/e2e/annotations_test.dart diff --git a/test/e2e/annotations_test.dart b/test/e2e/annotations_test.dart new file mode 100644 index 0000000..c8db69f --- /dev/null +++ b/test/e2e/annotations_test.dart @@ -0,0 +1,100 @@ +@Tags(['e2e']) +library; + +import 'package:shutter/src/run/manifest.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import 'e2e_helpers.dart'; + +void main() { + test('a MultiPreview whose previews throw is an error shot', () async { + final root = await exampleCopy(); + writeFiles(root, { + 'lib/preview/boom_preview.dart': ''' +import 'package:flutter/widget_previews.dart'; +import 'package:flutter/widgets.dart'; + +final class Boom extends MultiPreview { + const Boom(); + + @override + List get previews => throw StateError('boom'); +} + +@Boom() +Widget boom() => const Text('x'); +''', + }); + await shutter(root, ['shot', 'lib/preview/boom_preview.dart']); + expect( + RunManifest.read(lastRun(root)).shots.single.error, + contains('boom'), + ); + }); + + test('a static const annotation is shot; a sealed class is not; a ' + 'non-widget is an error', () async { + final root = await exampleCopy(); + writeFiles(root, { + 'lib/preview/resolved_preview.dart': ''' +import 'package:flutter/widget_previews.dart'; +import 'package:flutter/widgets.dart'; + +sealed class Shape extends StatelessWidget { + @Preview(name: 'Sealed') + const Shape(); +} + +class Card extends StatelessWidget { + const Card(); + + static const kPreview = Preview(name: 'Static const', size: Size(80, 40)); + + @kPreview + static Widget viaStatic() => const Card(); + + @override + Widget build(BuildContext context) => const Text('card'); +} + +@Preview(name: 'Not a widget', size: Size(80, 40)) +int notAWidget() => 1; +''', + }); + await shutter(root, ['shot', 'lib/preview/resolved_preview.dart']); + final resolved = { + for (final s in RunManifest.read(lastRun(root)).shots) s.name: s, + }; + expect(resolved.keys, {'Static const', 'Not a widget'}); + expect(resolved['Static const']!.status, ShotStatus.ok); + expect( + resolved['Not a widget']!.at, + startsWith('lib/preview/resolved_preview.dart:'), + ); + }); + + test("a library's class that shadows a Flutter one compiles", () async { + final root = await exampleCopy(); + writeFiles(root, { + 'lib/preview/shadow_preview.dart': ''' +import 'package:flutter/material.dart'; +import 'package:flutter/widget_previews.dart'; + +/// Shadows Material's Badge, as the library is allowed to. +class Badge extends StatelessWidget { + @Preview(name: 'Shadow / Badge', size: Size(80, 40)) + const Badge({super.key}); + + @override + Widget build(BuildContext context) => const Text('own'); +} +''', + }); + final shadow = await shutter(root, [ + 'shot', + 'lib/preview/shadow_preview.dart', + ]); + expect(shadow.exitCode, 0, reason: shadow.stdout + shadow.stderr); + }); +} diff --git a/test/e2e/capture_test.dart b/test/e2e/capture_test.dart index b0e8b20..da01cd3 100644 --- a/test/e2e/capture_test.dart +++ b/test/e2e/capture_test.dart @@ -31,44 +31,48 @@ Widget menu() => Align( '''; void main() { - test('--capture screen holds the viewport and what opens above the ' - 'preview', () async { - final root = await exampleCopy(); - Future<(String, Map)> shoot(List args) => - shootIn(root, args); - writeFiles(root, { - 'lib/preview/menu_state_preview.dart': menuPreview, - 'lib/preview/route_preview.dart': routePreview, - 'lib/preview/box_preview.dart': ''' + test( + '--capture screen draws the viewport at the scale of the preview', + () async { + final root = await exampleCopy(); + writeFiles(root, { + 'lib/preview/box_preview.dart': ''' import 'package:flutter/widget_previews.dart'; import 'package:flutter/widgets.dart'; @Preview(name: 'Box', size: Size(100, 50)) Widget box() => const ColoredBox(color: Color(0xFF0000FF)); ''', - }); - // The screen is drawn at the same scale as the preview: a 100x50 box - // at the top left of a 200x100 viewport fills a quarter of the image. - final (boxRun, boxShots) = await shoot([ - 'lib/preview/box_preview.dart', - '--capture', - 'screen', - '--viewport', - '200x100', - ]); - final boxImage = img.decodePng( - File(p.join(boxRun, boxShots['Box']!.png!)).readAsBytesSync(), - )!; - expect((boxImage.width, boxImage.height), (400, 200)); - bool blue(int x, int y) { - final pixel = boxImage.getPixel(x, y); - return (pixel.r, pixel.g, pixel.b) == (0, 0, 255); - } + }); + // The screen is drawn at the same scale as the preview: a 100x50 box + // at the top left of a 200x100 viewport fills a quarter of the image. + final (boxRun, boxShots) = await shootIn(root, [ + 'lib/preview/box_preview.dart', + '--capture', + 'screen', + '--viewport', + '200x100', + ]); + final boxImage = img.decodePng( + File(p.join(boxRun, boxShots['Box']!.png!)).readAsBytesSync(), + )!; + expect((boxImage.width, boxImage.height), (400, 200)); + bool blue(int x, int y) { + final pixel = boxImage.getPixel(x, y); + return (pixel.r, pixel.g, pixel.b) == (0, 0, 255); + } - expect(blue(199, 99), isTrue); - expect(blue(201, 50), isFalse); - expect(blue(50, 101), isFalse); + expect(blue(199, 99), isTrue); + expect(blue(201, 50), isFalse); + expect(blue(50, 101), isFalse); + }, + ); + test('--capture screen holds a menu that opens below the preview', () async { + final root = await exampleCopy(); + Future<(String, Map)> shoot(List args) => + shootIn(root, args); + writeFiles(root, {'lib/preview/menu_state_preview.dart': menuPreview}); // The menu's second item lies below the 60-high preview, in the // screen only. const menu = 'lib/preview/menu_state_preview.dart'; @@ -98,12 +102,22 @@ Widget box() => const ColoredBox(color: Color(0xFF0000FF)); } } expect(differing, greaterThan(0)); + }); + test('--capture screen holds a pushed page; --settle lets its transition ' + 'end', () async { + final root = await exampleCopy(); + writeFiles(root, {'lib/preview/route_preview.dart': routePreview}); // A page a tap pushes is in the screen; its transition takes 450 ms, // still under way at the default 300, over by 700. const route = ['lib/preview/route_preview.dart', '--tap', 'text:Go']; - Future settled(String ms) async => - (await shoot([...route, '--capture', 'screen', '--settle', ms])).$1; + Future settled(String ms) async => (await shootIn(root, [ + ...route, + '--capture', + 'screen', + '--settle', + ms, + ])).$1; final at300 = await settled('300'); final pushed = RunManifest.read(at300).shots.single; expect(pushed.status, ShotStatus.ok, reason: pushed.error); diff --git a/test/e2e/enter_test.dart b/test/e2e/enter_test.dart index a2dc316..3044b52 100644 --- a/test/e2e/enter_test.dart +++ b/test/e2e/enter_test.dart @@ -84,9 +84,13 @@ class _GreetingState extends State<_Greeting> { noField['Form']!.error, 'enter text:Submit=x: the widget holds no text field', ); + }); - // Fields without keys are named by their labels: the example's - // LoginForm, as the app has it. + test('--enter names a field without a key by its label', () async { + final root = await exampleCopy(); + Future<(String, Map)> shoot(List args) => + shootIn(root, args); + // The example's LoginForm, as the app has it. const login = [ '--widget', 'const LoginForm()', diff --git a/test/e2e/errors_test.dart b/test/e2e/errors_test.dart index f638216..988b883 100644 --- a/test/e2e/errors_test.dart +++ b/test/e2e/errors_test.dart @@ -68,37 +68,9 @@ Widget broken() => const DoesNotExist(); }, ); - test('shadowing, throwing, resolved, unpainted, and crashing previews ' - 'are reported', () async { + test('a preview that ends the test process has no result', () async { final root = await exampleCopy(); writeFiles(root, { - 'lib/preview/shadow_preview.dart': ''' -import 'package:flutter/material.dart'; -import 'package:flutter/widget_previews.dart'; - -/// Shadows Material's Badge, as the library is allowed to. -class Badge extends StatelessWidget { - @Preview(name: 'Shadow / Badge', size: Size(80, 40)) - const Badge({super.key}); - - @override - Widget build(BuildContext context) => const Text('own'); -} -''', - 'lib/preview/boom_preview.dart': ''' -import 'package:flutter/widget_previews.dart'; -import 'package:flutter/widgets.dart'; - -final class Boom extends MultiPreview { - const Boom(); - - @override - List get previews => throw StateError('boom'); -} - -@Boom() -Widget boom() => const Text('x'); -''', 'lib/preview/crash_preview.dart': ''' import 'dart:io'; @@ -109,60 +81,14 @@ import 'package:flutter/widgets.dart'; Widget crash() => exit(3); ''', }); - await shutter(root, ['shot', 'lib/preview/boom_preview.dart']); - expect( - RunManifest.read(lastRun(root)).shots.single.error, - contains('boom'), - ); - - writeFiles(root, { - 'lib/preview/resolved_preview.dart': ''' -import 'package:flutter/widget_previews.dart'; -import 'package:flutter/widgets.dart'; - -sealed class Shape extends StatelessWidget { - @Preview(name: 'Sealed') - const Shape(); -} - -class Card extends StatelessWidget { - const Card(); - - static const kPreview = Preview(name: 'Static const', size: Size(80, 40)); - - @kPreview - static Widget viaStatic() => const Card(); - - @override - Widget build(BuildContext context) => const Text('card'); -} - -@Preview(name: 'Not a widget', size: Size(80, 40)) -int notAWidget() => 1; -''', - }); - await shutter(root, ['shot', 'lib/preview/resolved_preview.dart']); - final resolved = { - for (final s in RunManifest.read(lastRun(root)).shots) s.name: s, - }; - expect(resolved.keys, {'Static const', 'Not a widget'}); - expect(resolved['Static const']!.status, ShotStatus.ok); - expect( - resolved['Not a widget']!.at, - startsWith('lib/preview/resolved_preview.dart:'), - ); - - final shadow = await shutter(root, [ - 'shot', - 'lib/preview/shadow_preview.dart', - ]); - expect(shadow.exitCode, 0, reason: shadow.stdout + shadow.stderr); - await shutter(root, ['shot', 'lib/preview/crash_preview.dart']); final crash = RunManifest.read(lastRun(root)).shots.single; expect(crash.status, ShotStatus.error); expect(crash.error, startsWith('no result')); + }); + test('a shell that drops its child paints nothing', () async { + final root = await exampleCopy(); writeFiles(root, { 'lib/preview/shell.dart': ''' import 'package:flutter/widgets.dart';