How to find off-screen ListView child in widget tests? - flutter

When displaying multiple children in a ListView, if a child is off-screen it can't be found by a widget test. Here's a full example:
main.dart
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: Scaffold(body: Test()));
}
}
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Container(
height: 600,
color: Colors.red,
),
Text("Find me!"),
],
);
}
}
main_test.dart
import 'package:flutter_app/main.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets("Find text", (WidgetTester tester) async {
final testableWidget = App();
await tester.pumpWidget(testableWidget);
expect(find.text("Find me!"), findsOneWidget);
});
}
This test fails, however if I change the height of the Container in main.dart to 599 it works.
Anyone know why this happens? Is it a bug? Is there a way around it?

Setting skipOffstate to false in your Finder is an approach. Try this:
expect(find.text("Find me!", skipOffstage: false), findsOneWidget);

Tests should behave as your app would do, otherwise, your tests become useless (since you're not testing the real behavior).
As such, this is not a bug.
You have to manually scroll the ListView inside your tests to make it load more widgets.
This can be done using tester:
final gesture = await tester.startGesture(Offset.zero /* THe position of your listview */ );
// Manual scroll
await gesture.moveBy(const Offset(0, 100));
await tester.pump(); // flush the widget tree

I highly recommend you to pay attention in the "Cartesian plane" of your screen/dragging movement.
Let me explain:
You should use:
await tester.drag(keyCartItemProduct1, Offset(-500.0, 0.0));
However, your "Offset" Command, must obey the same "Cartesian direction" than your Dragging.
2.1) Therefore: (The command Offset uses Cartesian 'directions') - lets see:
a) Left Dragging: Offset(-500.0, 0.0)
b) Right Dragging: Offset(+500.0, 0.0)
c) Up Dragging: Offset(0.0, +500.0)
d) Down Dragging: Offset(0.0, -500.0)

dragUntilVisible helps to scroll Listview or SingleChildScrollView to scroll till the expected widget is visible
final expectedWidget = find.byText("Find me!");
await tester.dragUntilVisible(
expectedWidget, // what you want to find
find.byType(ListView),
// widget you want to scroll
const Offset(0, 500), // delta to move
duration: Duration(seconds: 2));

try this code with skipOffstage set to false, it works fine.
testWidgets('Find widget off of screen', (WidgetTester tester) async {
await tester.pumpWidget(yourScreen);
expect(find.byKey(const Key('widgetname'), skipOffstage: false), findsOneWidget); });

Related

Widget testing DropdownButton finds duplicate DropdownMenuItems

I'm trying to write widget tests for a DropdownButton in my app. I noticed that after tapping the button to open it the call to find.byType(DropdownMenuItem) is returning double the expected number of DropdownMenuItems.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
enum MyColor {
blue,
green,
red,
yellow,
black,
pink
}
Future<void> main() async {
// runApp(MyApp());
// tests
group('dropdown tests', () {
testWidgets('how many elements should be found?', (tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
expect(find.byType(DropdownButton<MyColor>), findsOneWidget);
await tester.tap(find.byType(DropdownButton<MyColor>));
await tester.pumpAndSettle();
// fails
// expect(find.byType(DropdownMenuItem<MyColor>), findsNWidgets(MyColor.values.length));
// passes
expect(find.byType(DropdownMenuItem<MyColor>), findsNWidgets(MyColor.values.length * 2));
});
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
MyColor selected = MyColor.blue;
#override
Widget build(BuildContext context) {
return DropdownButton<MyColor>(
value: selected,
items: MyColor.values.map((col) {
return DropdownMenuItem<MyColor>(
child: Text(col.name),
value: col,
);
}).toList(),
onChanged: (value) {
if (value == null) {
return;
}
print('${value.name} selected');
setState(() {
selected = value;
});
}
);
}
}
Dartpad: https://dartpad.dev/?id=ce3eadff6bd98e6005817c70883451a0
I suspect that this has something to do with how Flutter renders the scene. I looked into the widget tests for the dropdown in the Flutter repo but I don't see any difference between my setup and theirs, but I also don't see any calls to find.byType(DropdownMenuItem). Does anyone know why this happens? Or is there an error in my code?
When an DropdownButton is rendered initially all items are rendered with IndexedStack and based on the selected value we see one visible item at the top
At that stage find.byType(DropdownMenuItem<MyColor>) will find 6
items
Once you tap on DropdownButton a _DropdownRoute route is pushed with all the items
At that stage find.byType(DropdownMenuItem<MyColor>) will find 12
items (the first 6 items are from IndexedStack and the second 6
items are from the new route)
So the number of items should be double at this stage as documented in the flutter tests as well
// Each item appears twice, once in the menu and once
// in the
dropdown button's IndexedStack.
https://github.com/flutter/flutter/blob/504e66920005937b6ffbc3ccd6b59d594b0e98c4/packages/flutter/test/material/dropdown_test.dart#L2230
Once you tap on of the DropdownMenuItem items the number of found widgets will go back to 6

Flutter - rebuild GestureDetector widget without user interaction

I am trying to display 3 images one after another by using GestureDetector and its onTap method but one image should be displayed without tapping. Supposing I have three images 1.png, 2.png, 3.png I would like to display the first image 1.png at the first place. Clicking on it will result in displaying the second image 2.png.
The second image 2.png should now be replaced after x seconds by displaying the image 3.png.
I am using a stateful widget that stores which image is currently shown. For the first image change the onTap will execute setState( () {screen = "2.png"} and the widget is rebuilt. However, I am pretty stuck now when the third image should be displayed after x seconds delay because there should not be any user interaction and I am not sure where to change it. I tried a lot of stuff especially editting the state from outside the widget but I did not succeed til now.
This is the code I am using:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Navigation Basics',
home: MyScreen(),
));
}
class MyScreen extends StatefulWidget {
#override
_MyScreenState createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
String currentScreen = 'assets/images/1.png';
#override
Widget build(BuildContext context) {
return GestureDetector(
child: FittedBox(
child: Container(
child: Image.asset(currentScreen),
),
fit: BoxFit.fill,
),
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
}
},
);
}
}
Displaying the second image worked without any issues but I have no idea what/where to code in order to display the third image.
Thanks for any help!
I'm assuming you don't want the second and third picture to respond to the gesture detection. Try this, please.
onTap: () {
if (currentScreen == 'assets/images/1.png') {
setState(() {
currentScreen = 'assets/images/2.png';
});
Future.delayed(const Duration(milliseconds: 3000), () {
setState(() {
currentScreen = 'assets/images/3.png';
});
});
} else {
return;
}
},

How to verify a widget is "offscreen"

bounty info: I'll accept your answer if:
isn't something along the line do this instead
the code sample is mostly unchanged
produce successful test, not just some quote from docs
doesn't need any extra package
[edit : 07/02/21] following Miyoyo#5957 on flutter community on
discord #iapicca Convert widget position to global, get width height, add both, and see if the resulting bottom right position is on screen? and using the following answers as reference:
test widget global position
test widget size
flutter_test dimensions issue
given the code sample below (also runnable on dartpad)
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
final _testKey = GlobalKey();
const _fabKey = ValueKey('fab');
final _onScreen = ValueNotifier<bool>(true);
void main() => runApp(_myApp);
const _myApp = MaterialApp(
home: Scaffold(
body: MyStage(),
floatingActionButton: MyFAB(),
),
);
class MyFAB extends StatelessWidget {
const MyFAB() : super(key: const ValueKey('MyFAB'));
#override
Widget build(BuildContext context) => FloatingActionButton(
key: _fabKey,
onPressed: () => _onScreen.value = !_onScreen.value,
);
}
class MyStage extends StatelessWidget {
const MyStage() : super(key: const ValueKey('MyStage'));
#override
Widget build(BuildContext context) => Stack(
children: [
ValueListenableBuilder(
child: FlutterLogo(
key: _testKey,
),
valueListenable: _onScreen,
builder: (context, isOnStage, child) => AnimatedPositioned(
top: MediaQuery.of(context).size.height *
(_onScreen.value ? .5 : -1),
child: child,
duration: const Duration(milliseconds: 100),
),
),
],
);
}
I want to test is the widget is off screen
here's the test code so far
void main() {
testWidgets('...', (tester) async {
await tester.pumpWidget(_myApp);
final rect = _testKey.currentContext.findRenderObject().paintBounds;
expect(tester.getSize(find.byKey(_testKey)), rect.size,
reason: 'size should match');
final lowestPointBefore = rect.bottomRight.dy;
print('lowest point **BEFORE** $lowestPointBefore ${DateTime.now()}');
expect(lowestPointBefore > .0, true, reason: 'should be on-screen');
await tester.tap(find.byKey(_fabKey));
await tester.pump(const Duration(milliseconds: 300));
final lowestPointAfter =
_testKey.currentContext.findRenderObject().paintBounds.bottomRight.dy;
print('lowest point **AFTER** $lowestPointAfter ${DateTime.now()}');
expect(lowestPointAfter > .0, false, reason: 'should be off-screen');
});
}
and the logs produced
00:03 +0: ...
lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
Expected: <false>
Actual: <true>
When the exception was thrown, this was the stack:
#4 main.<anonymous closure> (file:///home/francesco/projects/issue/test/widget_test.dart:83:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
This was caught by the test expectation on the following line:
file:///home/francesco/projects/issue/test/widget_test.dart line 83
The test description was:
...
════════════════════════════════════════════════════════════════════════════════════════════════════
00:03 +0 -1: ... [E]
Test failed. See exception logs above.
The test description was: ...
00:03 +0 -1: Some tests failed.
I'm not sure if my approach is correct
and the time in the print suggest me that
lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733
suggest me that
await tester.pumpAndSettle(Duration(milliseconds: 300));
doesn't do what I think it does
Problems are:
We were trying to find the rect of FlutterLogo but FlutterLogo rect will remain same the parent AnimatedPositioned widget's location are actually changing.
Even though we now start to check for AnimatedPositioned paintBounds it will still be the same as we are not changing width but the position it self.
Solution:
Get the screen rect by topWidget for me it's Scaffold. (if we have different widgets like HomeScreen which contains FAB button we just need to find that rect)
Before click I'm checking if fab button is on-screen or not
Tap and pump the widget and let it settle.
Search for widget rect and it will be out of the screen i.e. in our case -600
Added comments in the code it self
testWidgets('...', (tester) async {
await tester.pumpWidget(MyApp);
//check screen width height - here I'm checking for scaffold but you can put some other logic for screen size or parent widget type
Rect screenRect = tester.getRect(find.byType(Scaffold));
print("screenRect: $screenRect");
//checking previous position of the widget - on our case we are animating widget position via AnimatedPositioned
// which in itself is a statefulwidget and has Positioned widget inside
//also if we have multiple widgets of same type give them uniqueKey
AnimatedPositioned widget =
tester.firstWidget(find.byType(AnimatedPositioned));
double topPosition = widget.top;
print(widget);
print("AnimatedPositioned topPosition: $topPosition}");
expect(
screenRect.bottom > topPosition && screenRect.top < topPosition, true,
reason: 'should be on-screen');
//click button to animate the widget and wait
await tester.tap(find.byKey(fabKey));
//this will wait for animation to settle or call pump after duration
await tester.pumpAndSettle(const Duration(milliseconds: 300));
//check after position of the widget
AnimatedPositioned afterAnimationWidget =
tester.firstWidget(find.byType(AnimatedPositioned));
double afterAnimationTopPosition = afterAnimationWidget.top;
Rect animatedWidgetRect = tester.getRect(find.byType(AnimatedPositioned));
print("rect of widget : $animatedWidgetRect");
expect(
screenRect.bottom > afterAnimationTopPosition &&
screenRect.top < afterAnimationTopPosition,
false,
reason: 'should be off-screen');
});
Note: replaced _ from code as it was hiding the object from test file.
Output:
screenRect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
fab clicked
rect of widget : Rect.fromLTRB(0.0, -600.0, 24.0, -576.0)
I found this answer (in particular the code inside the onNotification), which kind of does what (I think) you want. It finds the RenderObject using the current context of the key. Afterwards it finds the RenderAbstractViewport using this RenderObject, and checks the offSetToReveal. Using this offset you can determine whether the current RenderObject is being displayed or not (using a simple comparison).
I'm not a 100% sure this will work / is what you want, but hopefully it can push you in the right direction.
Also (even though you stated you didn't want any external package), on the same question someone recommended this package, which can be useful for others having the same problem but who are open to using an external package.
I want to thank #parth-dave
for his answer, that I happily reward with the bounty
and Miyoyo referenced in the question
I want to offer my own implementation built on his approach
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
// !! uncomment tge line below to run as test app
// void main() => runApp(_myApp);
class Keys {
static final subject = UniqueKey();
static final parent = UniqueKey();
static final trigger = UniqueKey();
}
final _onScreen = ValueNotifier<bool>(true);
Widget get app => MaterialApp(
home: Scaffold(
key: Keys.parent,
body: MyStage(),
floatingActionButton: MyFAB(),
),
);
class MyFAB extends StatelessWidget {
const MyFAB() : super(key: const ValueKey('MyFAB'));
#override
Widget build(BuildContext context) => FloatingActionButton(
key: Keys.trigger,
onPressed: () => _onScreen.value = !_onScreen.value,
);
}
class MyStage extends StatelessWidget {
const MyStage() : super(key: const ValueKey('MyStage'));
#override
Widget build(BuildContext context) => Stack(
children: [
ValueListenableBuilder(
child: FlutterLogo(
key: Keys.subject,
),
valueListenable: _onScreen,
builder: (context, isOnStage, child) => AnimatedPositioned(
top: MediaQuery.of(context).size.height *
(_onScreen.value ? .5 : -1),
child: child,
duration: const Duration(milliseconds: 100),
),
),
],
);
}
void main() {
group('`AnimatedPositined` test', () {
testWidgets(
'WHEN no interaction with `trigger` THEN `subject` is ON SCREEN',
(tester) async {
await tester.pumpWidget(app);
final parent = tester.getRect(find.byKey(Keys.parent));
final subject = tester.getRect(find.byKey(Keys.subject));
expect(parent.overlaps(subject), true, reason: 'should be ON-screen');
});
testWidgets('WHEN `trigger` tapped THEN `subject` is OFF SCREEN`',
(tester) async {
await tester.pumpWidget(app);
await tester.tap(find.byKey(Keys.trigger));
await tester.pumpAndSettle(const Duration(milliseconds: 300));
final parent = tester.getRect(find.byKey(Keys.parent));
final subject = tester.getRect(find.byKey(Keys.subject));
expect(parent.overlaps(subject), false, reason: 'should be OFF-screen');
});
});
}

Flutter Camera Plugin taking dark image bug

I am getting dark images from flutter Camera Plugin.
Camera Preview is showing correctly but after taking the picture it becomes too dark.
I searched and what i found that it's about the FPS and exposure of the camera.
How can I solve this problem?
I need to show camera preview and take pictures in my app.
Please don't tell me to use image_picker package.
Device : Redmi note 4
Android OS : 7.0
Here is the Image
dark image
Here is the code
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';
Future<void> main() async {
// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();
// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first;
runApp(
MaterialApp(
theme: ThemeData.dark(),
home: TakePictureScreen(
// Pass the appropriate camera to the TakePictureScreen widget.
camera: firstCamera,
),
),
);
}
// A screen that allows users to take a picture using a given camera.
class TakePictureScreen extends StatefulWidget {
final CameraDescription camera;
const TakePictureScreen({
Key key,
#required this.camera,
}) : super(key: key);
#override
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> {
CameraController _controller;
Future<void> _initializeControllerFuture;
#override
void initState() {
super.initState();
// To display the current output from the Camera,
// create a CameraController.
_controller = CameraController(
// Get a specific camera from the list of available cameras.
widget.camera,
// Define the resolution to use.
ResolutionPreset.medium,
);
// Next, initialize the controller. This returns a Future.
_initializeControllerFuture = _controller.initialize();
}
#override
void dispose() {
// Dispose of the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Take a picture')),
// Wait until the controller is initialized before displaying the
// camera preview. Use a FutureBuilder to display a loading spinner
// until the controller has finished initializing.
body: FutureBuilder<void>(
future: _initializeControllerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If the Future is complete, display the preview.
return CameraPreview(_controller);
} else {
// Otherwise, display a loading indicator.
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.camera_alt),
// Provide an onPressed callback.
onPressed: () async {
// Take the Picture in a try / catch block. If anything goes wrong,
// catch the error.
try {
// Ensure that the camera is initialized.
await _initializeControllerFuture;
// Construct the path where the image should be saved using the
// pattern package.
final path = join(
// Store the picture in the temp directory.
// Find the temp directory using the `path_provider` plugin.
(await getTemporaryDirectory()).path,
'${DateTime.now()}.png',
);
// Attempt to take a picture and log where it's been saved.
await _controller.takePicture(path);
// If the picture was taken, display it on a new screen.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DisplayPictureScreen(imagePath: path),
),
);
} catch (e) {
// If an error occurs, log the error to the console.
print(e);
}
},
),
);
}
}
// A widget that displays the picture taken by the user.
class DisplayPictureScreen extends StatelessWidget {
final String imagePath;
const DisplayPictureScreen({Key key, this.imagePath}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Display the Picture')),
// The image is stored as a file on the device. Use the `Image.file`
// constructor with the given path to display the image.
body: Image.file(File(imagePath)),
);
}
}
Just put delay before take picture.
Future.delayed(const Duration(milliseconds: 500), () {
_controller.takePicture(path);
});
I think it's not about a delay, images are dark if exposure is not handled.
Also exposure requires focus pre captures to work and are not handled in official plugin now.
You can use this plugin : CamerAwesome
Official plugin has been quite abandonned. This plugin includes flash, zoom, auto focus, exposure... and no initialisation required.
It uses value notifier to change data directly in preview like this :
// init Notifiers
ValueNotifier<CameraFlashes> _switchFlash = ValueNotifier(CameraFlashes.NONE);
ValueNotifier<Sensors> _sensor = ValueNotifier(Sensors.BACK);
ValueNotifier<Size> _photoSize = ValueNotifier(null);
#override
Widget build(BuildContext context) {
return CameraAwesome(
onPermissionsResult: (bool result) { }
selectDefaultSize: (List<Size> availableSizes) => Size(1920, 1080),
onCameraStarted: () { },
onOrientationChanged: (CameraOrientations newOrientation) { },
zoom: 0.64,
sensor: _sensor,
photoSize: _photoSize,
switchFlashMode: _switchFlash,
orientation: DeviceOrientation.portraitUp,
fitted: true,
);
};
A hack that works for me, with camera plugin: take the picture twice. The first one buys time for the second one to have the proper exposure and focus.
final image = await controller.takePicture(); // is not used
final image2 = await controller.takePicture();

How to test Flutter widgets on different screen sizes?

I have a Flutter widget which shows extra data depending on the screen size. Does anyone know a way of testing this widget on multiple different screen sizes?
I've had a look through the widget_tester source code but can't find anything.
You can specify custom surface size by using WidgetTester
The following code will run a test with a screen size of 42x42
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets("foo", (tester) async {
tester.binding.window.physicalSizeTestValue = Size(42, 42);
// resets the screen to its original size after the test end
addTearDown(tester.binding.window.clearPhysicalSizeTestValue);
// TODO: do something
});
}
Not sure why but solution of #rémi-rousselet didn't work for me. I've had to specify screen size using binding.window.physicalSizeTestValue and binding.window.devicePixelRatioTestValue so that output is fully deterministic
I've added a little bit more code for flutter beginners like me. Check this:
void main() {
final TestWidgetsFlutterBinding binding =
TestWidgetsFlutterBinding.ensureInitialized();
testWidgets("Basic layout test (mobile device)", (tester) async {
binding.window.physicalSizeTestValue = Size(400, 200);
binding.window.devicePixelRatioTestValue = 1.0;
await tester.pumpWidget(new MyApp());
expect(find.byType(MyHomePage), findsOneWidget);
// etc.
});
}
There is a package called device_preview that can simulate your flutter app running on different devices.
#rémi-rousselet's solution works perfectly!
In addition if you want to test an orientation change, try this:
const double PORTRAIT_WIDTH = 400.0;
const double PORTRAIT_HEIGHT = 800.0;
const double LANDSCAPE_WIDTH = PORTRAIT_HEIGHT;
const double LANDSCAPE_HEIGHT = PORTRAIT_WIDTH;
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.setSurfaceSize(Size(PORTRAIT_WIDTH, PORTRAIT_HEIGHT));
await tester.pumpWidget(MyWidget());
// test in portrait
await binding.setSurfaceSize(Size(LANDSCAPE_WIDTH, LANDSCAPE_HEIGHT));
await tester.pumpAndSettle();
// OrientationBuilder gets triggered
// test in landscape
Currently the safest way is to use setSurfaceSize
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets("foo", (tester) async {
tester.binding.setSurfaceSize(Size(400, 400));
// reset
tester.binding.setSurfaceSize(null);
// continue
});
}
See here for related Github issue
You could try this widget to test your widgets changing screen size in realtime
Screen Size Test
https://pub.dev/packages/screen_size_test
Preview
Demo
https://dartpad.dartlang.org/43d9c47a8bf031ce3ef2f6314c9dbd52
Code Sample
import 'package:screen_size_test/screen_size_test.dart';
...
MaterialApp(
title: 'Demo',
builder: (context, child) => ScreenSizeTest(
child: child,
),
home: Scaffold(
body: ListView(
children: List.generate(
20,
(index) => Container(
padding: EdgeInsets.all(10),
child: Placeholder(),
)),
),
),
)
Although #Rémi Rousselet's answer was very helpful it didn't completely solve my problem. It turns out that I could just wrap my widget under test in a MediaQuery widget and set the size.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
Widget makeTestableWidget({Widget child, Size size}) {
return MaterialApp(
home: MediaQuery(
data: MediaQueryData(size: size),
child: child,
),
);
}
testWidgets("tablet", (tester) async {
final testableWidget = makeTestableWidget(
child: WidgetUnderTest(),
size: Size(1024, 768),
);
...
});
testWidgets("phone", (tester) async {
final testableWidget = makeTestableWidget(
child: WidgetUnderTest(),
size: Size(375, 812),
);
...
});
}