Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions test/e2e/annotations_test.dart
Original file line number Diff line number Diff line change
@@ -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<Preview> 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);
});
}
78 changes: 46 additions & 32 deletions test/e2e/capture_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Shot>)> shoot(List<String> 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<String, Shot>)> shoot(List<String> 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';
Expand Down Expand Up @@ -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<String> settled(String ms) async =>
(await shoot([...route, '--capture', 'screen', '--settle', ms])).$1;
Future<String> 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);
Expand Down
8 changes: 6 additions & 2 deletions test/e2e/enter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Shot>)> shoot(List<String> args) =>
shootIn(root, args);
// The example's LoginForm, as the app has it.
const login = [
'--widget',
'const LoginForm()',
Expand Down
82 changes: 4 additions & 78 deletions test/e2e/errors_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Preview> get previews => throw StateError('boom');
}

@Boom()
Widget boom() => const Text('x');
''',
'lib/preview/crash_preview.dart': '''
import 'dart:io';

Expand All @@ -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';
Expand Down