在探索 flutter 时,我尝试创建一个小部件,它从内存中渲染图像(至少目前如此)并在其上方显示
IconButton
。要校正 IconButton
的大小,我需要获取图像的实际大小。因此,要设置 iconSize
,我将 FutureBuilder
与 Completer
和 Image.resolve(...).addListener
结合使用。
它按预期工作。但我很难让小部件测试通过,因为它从不渲染图像(在加载图像时,测试图像数据的期望通过)并且从不创建
IconButton
小部件,所以我的检查没有通过。
这应该是
testWidgets
使用的 FakeAsync 的东西。 tester.pump
应该进步假时间,但即使我在一个循环中运行pump
,IconButton
小部件也永远不会出现。可以使用 ImageStream b
class Thumbnail extends StatelessWidget {
final Uint8List thumbnail;
final ImagePage mainImage;
final Image thumbnailImage;
final bool editMode;
final completer = Completer<ui.Image>();
Thumbnail(
{super.key,
required this.thumbnail,
required this.mainImage,
this.editMode = false})
: thumbnailImage = Image.memory(thumbnail);
@override
Widget build(BuildContext context) {
thumbnailImage.image
.resolve(const ImageConfiguration())
.addListener(ImageStreamListener((image, synchronousCall) {
completer.complete(image.image);
image.dispose();
}));
return Stack(alignment: Alignment.center, children: [
thumbnailImage,
FutureBuilder<ui.Image>(
future: completer.future,
builder: (context, snapshot) {
return Offstage(
offstage: !snapshot.hasData,
child: makeIconButton(context, snapshot.data));
}),
]);
}
static int minSize(ui.Image? image) {
if (image == null) {
return 2;
}
return image.width < image.height ? image.width : image.height;
}
Widget makeIconButton(BuildContext context, ui.Image? image) {
final imageMinSize = minSize(image);
return IconButton(
onPressed: () {
if (!editMode) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => page.Page.fromWidget(mainImage)));
}
},
icon: editMode ? const Icon(Icons.edit) : const Icon(Icons.zoom_in),
iconSize: imageMinSize >= 64 ? 48.0 : imageMinSize / 2,
splashRadius: imageMinSize / 2,
color: Colors.blueGrey.withOpacity(0.8),
hoverColor: Colors.lightBlue.withOpacity(0.3),
);
}
}
测试代码为:
void main() {
testWidgets('Thumbnail', (WidgetTester tester) async {
var thumbnailPage = page.Page('Thumbnail', const [],
thumbnail_package.Thumbnail(
thumbnail: image_samples.thumbnail_64x64,
mainImage: thumbnail_package.ImagePage(
'MainImage',
MemoryImage(image_samples.thumbnail_64x64)
)
));
await tester.pumpWidget(
MaterialApp(home: thumbnailPage)
);
expect(find.byType(thumbnail_package.Thumbnail), findsOneWidget);
expect(find.byType(Image), findsOneWidget);
for (final image in find.byType(Image).evaluate().map((element) =>
element.widget as Image)) {
expect(
Uint8ListCompareByValue((image.image as MemoryImage).bytes),
Uint8ListCompareByValue(image_samples.thumbnail_64x64));
}
// while (!find
// .byType(IconButton)
// .hasFound) {
// await tester.pump();
// }
await tester.tap(find.byType(IconButton)); // <--- THIS CALL FAILS
await tester.pumpAndSettle();
expect(find.byType(thumbnail_package.Thumbnail), findsNothing);
expect(find.byType(Image), findsOneWidget);
for (final image in find.byType(Image).evaluate().map((element) =>
element.widget as Image)) {
expect(
Uint8ListCompareByValue((image.image as MemoryImage).bytes),
Uint8ListCompareByValue(image_samples.thumbnail_64x64));
}
});
我收到以下错误:
运行测试时抛出以下断言: 查找器“找到 0 个类型为“IconButton”的小部件:[]”(用于调用“tap()”)找不到任何匹配的小部件。
您能给我关于如何在测试中处理这种情况的任何建议吗?
好吧,问题是我对异步调用如何工作的错误期望:不要期望任何特殊的执行顺序。
在模拟器上的调试运行和首先调用 ImageStreamListener 的测试运行中,对 ImageStreamListener 和 FutureBuilder 的调用顺序不同。结果,FutureBuilder 是在已经完成的 future 中创建的。
FutureBuilder 本身的设计(对我来说有些奇怪的设计),这样如果 future 在第一次调用时准备就绪,它就不会第二次调用它的构建器回调。但在第一次调用时,它会传递等待状态和空快照。所以我们在这里,要么确保稍后准备好,要么检查未来而不是快照变量(如果您确定它已准备好)。
此代码工作正常,因为我在创建 FutureBuilder 后强制调用回调:
@override
Widget build(BuildContext context) {
final widget = Stack(alignment: Alignment.center, children: [
thumbnailImage,
FutureBuilder<ui.Image>(
future: completer.future,
builder: (context, snapshot) {
return Offstage(
offstage: !snapshot.hasData,
child: makeIconButton(context, snapshot.data));
}),
]);
thumbnailImage.image
.resolve(const ImageConfiguration())
.addListener(ImageStreamListener((image, synchronousCall) {
completer.complete(image.image);
image.dispose();
}));
return widget;
}
在水龙头前简单添加一个泵并沉淀:
await tester.pumpAndSettle(); // additional call to process async callback and rebuild widget
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
虽然我不完全确定这是否是一个稳定的解决方案,并且在发生更多异步调用的情况下它不会失败。