mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add tests for AssetVendor and resolution-dependent image loading
Fixes #2198
This commit is contained in:
parent
7a2d82d0af
commit
fca41bc2c5
@ -5,6 +5,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mojo/core.dart' as core;
|
||||
@ -39,25 +40,33 @@ class _ResolvingAssetBundle extends CachingAssetBundle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction for reading images out of a Mojo data pipe.
|
||||
///
|
||||
/// Useful for mocking purposes in unit tests.
|
||||
typedef Future<ui.Image> ImageDecoder(core.MojoDataPipeConsumer pipe);
|
||||
|
||||
// Asset bundle that understands how specific asset keys represent image scale.
|
||||
class _ResolutionAwareAssetBundle extends _ResolvingAssetBundle {
|
||||
_ResolutionAwareAssetBundle({
|
||||
AssetBundle bundle,
|
||||
_ResolutionAwareAssetResolver resolver
|
||||
_ResolutionAwareAssetResolver resolver,
|
||||
ImageDecoder imageDecoder
|
||||
}) : super(
|
||||
bundle: bundle,
|
||||
resolver: resolver
|
||||
);
|
||||
), _imageDecoder = imageDecoder;
|
||||
|
||||
_ResolutionAwareAssetResolver get resolver => super.resolver;
|
||||
|
||||
final ImageDecoder _imageDecoder;
|
||||
|
||||
Future<ImageInfo> fetchImage(String key) async {
|
||||
core.MojoDataPipeConsumer pipe = await load(key);
|
||||
// At this point the key should be in our key cache, and the image
|
||||
// resource should be in our image cache
|
||||
double scale = resolver.getScale(keyCache[key]);
|
||||
return new ImageInfo(
|
||||
image: await decodeImageFromDataPipe(pipe),
|
||||
image: await _imageDecoder(pipe),
|
||||
scale: scale
|
||||
);
|
||||
}
|
||||
@ -183,12 +192,14 @@ class AssetVendor extends StatefulComponent {
|
||||
Key key,
|
||||
this.bundle,
|
||||
this.devicePixelRatio,
|
||||
this.child
|
||||
this.child,
|
||||
this.imageDecoder: decodeImageFromDataPipe
|
||||
}) : super(key: key);
|
||||
|
||||
final AssetBundle bundle;
|
||||
final double devicePixelRatio;
|
||||
final Widget child;
|
||||
final ImageDecoder imageDecoder;
|
||||
|
||||
_AssetVendorState createState() => new _AssetVendorState();
|
||||
|
||||
@ -207,6 +218,7 @@ class _AssetVendorState extends State<AssetVendor> {
|
||||
void _initBundle() {
|
||||
_bundle = new _ResolutionAwareAssetBundle(
|
||||
bundle: config.bundle,
|
||||
imageDecoder: config.imageDecoder,
|
||||
resolver: new _ResolutionAwareAssetResolver(
|
||||
bundle: config.bundle,
|
||||
devicePixelRatio: config.devicePixelRatio
|
||||
|
@ -26,14 +26,12 @@ class BindingObserver {
|
||||
/// This is the glue that binds the framework to the Flutter engine.
|
||||
class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, MojoShell, Renderer {
|
||||
|
||||
WidgetFlutterBinding._();
|
||||
|
||||
/// Creates and initializes the WidgetFlutterBinding. This constructor is
|
||||
/// idempotent; calling it a second time will just return the
|
||||
/// previously-created instance.
|
||||
static WidgetFlutterBinding ensureInitialized() {
|
||||
if (_instance == null)
|
||||
new WidgetFlutterBinding._();
|
||||
new WidgetFlutterBinding();
|
||||
return _instance;
|
||||
}
|
||||
|
||||
|
225
packages/flutter/test/widget/asset_vendor_test.dart
Normal file
225
packages/flutter/test/widget/asset_vendor_test.dart
Normal file
@ -0,0 +1,225 @@
|
||||
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui show Image, hashValues;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:mojo/core.dart' as core;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class TestImage extends ui.Image {
|
||||
TestImage(this.scale);
|
||||
final double scale;
|
||||
int get width => (48*scale).floor();
|
||||
int get height => (48*scale).floor();
|
||||
void dispose() { }
|
||||
}
|
||||
|
||||
class TestMojoDataPipeConsumer extends core.MojoDataPipeConsumer {
|
||||
TestMojoDataPipeConsumer(this.scale) : super(null);
|
||||
final double scale;
|
||||
}
|
||||
|
||||
String testManifest = '''
|
||||
{
|
||||
"assets/image.png" : [
|
||||
"assets/1.5x/image.png",
|
||||
"assets/2.0x/image.png",
|
||||
"assets/3.0x/image.png",
|
||||
"assets/4.0x/image.png"
|
||||
]
|
||||
}
|
||||
''';
|
||||
|
||||
class TestAssetBundle extends AssetBundle {
|
||||
// Image loading logic routes through load(key)
|
||||
ImageResource loadImage(String key) => null;
|
||||
Future<String> loadString(String key) {
|
||||
if (key == 'AssetManifest.json')
|
||||
return (new Completer<String>()..complete(testManifest)).future;
|
||||
return null;
|
||||
}
|
||||
Future<core.MojoDataPipeConsumer> load(String key) {
|
||||
core.MojoDataPipeConsumer pipe = null;
|
||||
switch (key) {
|
||||
case 'assets/image.png':
|
||||
pipe = new TestMojoDataPipeConsumer(1.0);
|
||||
break;
|
||||
case 'assets/1.5x/image.png':
|
||||
pipe = new TestMojoDataPipeConsumer(1.5);
|
||||
break;
|
||||
case 'assets/2.0x/image.png':
|
||||
pipe = new TestMojoDataPipeConsumer(2.0);
|
||||
break;
|
||||
case 'assets/3.0x/image.png':
|
||||
pipe = new TestMojoDataPipeConsumer(3.0);
|
||||
break;
|
||||
case 'assets/4.0x/image.png':
|
||||
pipe = new TestMojoDataPipeConsumer(4.0);
|
||||
break;
|
||||
}
|
||||
return (new Completer<core.MojoDataPipeConsumer>()..complete(pipe)).future;
|
||||
}
|
||||
String toString() => '$runtimeType@$hashCode()';
|
||||
}
|
||||
|
||||
Future<ui.Image> testDecodeImageFromDataPipe(core.MojoDataPipeConsumer pipe) {
|
||||
TestMojoDataPipeConsumer testPipe = pipe as TestMojoDataPipeConsumer;
|
||||
assert(testPipe != null);
|
||||
ui.Image image = new TestImage(testPipe.scale);
|
||||
return (new Completer<ui.Image>()..complete(image)).future;
|
||||
}
|
||||
|
||||
Widget buildImageAtRatio(String image, Key key, double ratio, bool inferSize) {
|
||||
const double windowSize = 500.0; // 500 logical pixels
|
||||
const double imageSize = 200.0; // 200 logical pixels
|
||||
|
||||
return new MediaQuery(
|
||||
data: new MediaQueryData(
|
||||
size: const Size(windowSize, windowSize),
|
||||
devicePixelRatio: ratio,
|
||||
padding: const EdgeDims.all(0.0)
|
||||
),
|
||||
child: new AssetVendor(
|
||||
bundle: new TestAssetBundle(),
|
||||
devicePixelRatio: ratio,
|
||||
imageDecoder: testDecodeImageFromDataPipe,
|
||||
child: new Center(
|
||||
child: inferSize ?
|
||||
new AssetImage(
|
||||
key: key,
|
||||
name: image
|
||||
) :
|
||||
new AssetImage(
|
||||
key: key,
|
||||
name: image,
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
fit: ImageFit.fill
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
RenderImage getRenderImage(tester, Key key) {
|
||||
return tester.findElementByKey(key).renderObject as RenderImage;
|
||||
}
|
||||
|
||||
TestImage getTestImage(tester, Key key) {
|
||||
return getRenderImage(tester, key).image as TestImage;
|
||||
}
|
||||
|
||||
void pumpTreeToLayout(WidgetTester tester, Widget widget) {
|
||||
Duration pumpDuration = const Duration(milliseconds: 0);
|
||||
EnginePhase pumpPhase = EnginePhase.layout;
|
||||
tester.pumpWidget(widget, pumpDuration, pumpPhase);
|
||||
}
|
||||
|
||||
void main() {
|
||||
String image = 'assets/image.png';
|
||||
|
||||
test('Image for device pixel ratio 1.0', () {
|
||||
const double ratio = 1.0;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 1.0);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 1.0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Image for device pixel ratio 0.5', () {
|
||||
const double ratio = 0.5;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 1.0);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 1.0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Image for device pixel ratio 1.5', () {
|
||||
const double ratio = 1.5;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 1.5);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 1.5);
|
||||
});
|
||||
});
|
||||
|
||||
test('Image for device pixel ratio 1.75', () {
|
||||
const double ratio = 1.75;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 1.5);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 1.5);
|
||||
});
|
||||
});
|
||||
|
||||
test('Image for device pixel ratio 2.3', () {
|
||||
const double ratio = 2.3;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 2.0);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 2.0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Image for device pixel ratio 3.7', () {
|
||||
const double ratio = 3.7;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 4.0);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 4.0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Image for device pixel ratio 5.1', () {
|
||||
const double ratio = 5.1;
|
||||
testWidgets((WidgetTester tester) {
|
||||
Key key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false));
|
||||
expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
|
||||
expect(getTestImage(tester, key).scale, 4.0);
|
||||
key = new GlobalKey();
|
||||
pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true));
|
||||
expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
|
||||
expect(getTestImage(tester, key).scale, 4.0);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
@ -15,7 +15,8 @@ typedef Point SizeToPointFunction(Size size);
|
||||
/// This class provides hooks for accessing the rendering tree and dispatching
|
||||
/// fake tap/drag/etc. events.
|
||||
class Instrumentation {
|
||||
Instrumentation() : binding = WidgetFlutterBinding.ensureInitialized();
|
||||
Instrumentation({ WidgetFlutterBinding binding })
|
||||
: this.binding = binding ?? WidgetFlutterBinding.ensureInitialized();
|
||||
|
||||
final WidgetFlutterBinding binding;
|
||||
|
||||
|
@ -12,15 +12,69 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'instrumentation.dart';
|
||||
|
||||
/// Enumeration of possible phases to reach in pumpWidget.
|
||||
enum EnginePhase {
|
||||
layout,
|
||||
compositingBits,
|
||||
paint,
|
||||
composite,
|
||||
flushSemantics,
|
||||
sendSemanticsTree
|
||||
}
|
||||
|
||||
/// Helper class for fluter tests providing fake async.
|
||||
class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding {
|
||||
|
||||
/// Creates and initializes the binding. This constructor is
|
||||
/// idempotent; calling it a second time will just return the
|
||||
/// previously-created instance.
|
||||
static WidgetFlutterBinding ensureInitialized() {
|
||||
if (WidgetFlutterBinding.instance == null)
|
||||
new _SteppedWidgetFlutterBinding();
|
||||
return WidgetFlutterBinding.instance;
|
||||
}
|
||||
|
||||
EnginePhase phase = EnginePhase.sendSemanticsTree;
|
||||
|
||||
// Pump the rendering pipeline up to the given phase.
|
||||
void beginFrame() {
|
||||
buildDirtyElements();
|
||||
_beginFrame();
|
||||
Element.finalizeTree();
|
||||
}
|
||||
|
||||
// Cloned from Renderer.beginFrame() but with early-exit semantics.
|
||||
void _beginFrame() {
|
||||
assert(renderView != null);
|
||||
RenderObject.flushLayout();
|
||||
if (phase == EnginePhase.layout)
|
||||
return;
|
||||
RenderObject.flushCompositingBits();
|
||||
if (phase == EnginePhase.compositingBits)
|
||||
return;
|
||||
RenderObject.flushPaint();
|
||||
if (phase == EnginePhase.paint)
|
||||
return;
|
||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||
if (phase == EnginePhase.composite)
|
||||
return;
|
||||
if (SemanticsNode.hasListeners) {
|
||||
RenderObject.flushSemantics();
|
||||
if (phase == EnginePhase.flushSemantics)
|
||||
return;
|
||||
SemanticsNode.sendSemanticsTree();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper class for flutter tests providing fake async.
|
||||
///
|
||||
/// This class extends Instrumentation to also abstract away the beginFrame
|
||||
/// and async/clock access to allow writing tests which depend on the passage
|
||||
/// of time without actually moving the clock forward.
|
||||
class WidgetTester extends Instrumentation {
|
||||
WidgetTester._(FakeAsync async)
|
||||
: async = async,
|
||||
: super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()),
|
||||
async = async,
|
||||
clock = async.getClock(new DateTime.utc(2015, 1, 1)) {
|
||||
timeDilation = 1.0;
|
||||
ui.window.onBeginFrame = null;
|
||||
@ -32,7 +86,18 @@ class WidgetTester extends Instrumentation {
|
||||
|
||||
/// Calls [runApp()] with the given widget, then triggers a frame sequent and
|
||||
/// flushes microtasks, by calling [pump()] with the same duration (if any).
|
||||
void pumpWidget(Widget widget, [ Duration duration ]) {
|
||||
/// The supplied EnginePhase is the final phase reached during the pump pass;
|
||||
/// if not supplied, the whole pass is executed.
|
||||
void pumpWidget(Widget widget, [ Duration duration, EnginePhase phase ]) {
|
||||
if (binding is _SteppedWidgetFlutterBinding) {
|
||||
// Some tests call WidgetFlutterBinding.ensureInitialized() manually, so
|
||||
// we can't actually be sure we have a stepped binding.
|
||||
_SteppedWidgetFlutterBinding steppedBinding = binding;
|
||||
steppedBinding.phase = phase ?? EnginePhase.sendSemanticsTree;
|
||||
} else {
|
||||
// Can't step to a given phase in that case
|
||||
assert(phase == null);
|
||||
}
|
||||
runApp(widget);
|
||||
pump(duration);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user