Commit 1655ca80 authored by krisgiesing's avatar krisgiesing

Merge pull request #2405 from krisgiesing/scaling_test

Add tests for AssetVendor and resolution-dependent image loading
parents bbaff5ea 390fcd99
......@@ -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,34 @@ 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
}) : super(
_ResolutionAwareAssetResolver resolver,
ImageDecoder imageDecoder
}) : _imageDecoder = imageDecoder,
super(
bundle: bundle,
resolver: resolver
);
_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 +193,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 +219,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;
}
......
// 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,8 +12,61 @@ import 'package:flutter/widgets.dart';
import 'instrumentation.dart';
/// Enumeration of possible phases to reach in pumpWidget.
enum EnginePhase {
layout,
compositingBits,
paint,
composite,
flushSemantics,
sendSemanticsTree
}
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 fluter tests providing fake async.
/// 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
......@@ -21,7 +74,8 @@ import 'instrumentation.dart';
class WidgetTester extends Instrumentation {
WidgetTester._(FakeAsync async)
: async = async,
clock = async.getClock(new DateTime.utc(2015, 1, 1)) {
clock = async.getClock(new DateTime.utc(2015, 1, 1)),
super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()) {
timeDilation = 1.0;
ui.window.onBeginFrame = null;
runApp(new ErrorWidget()); // flush out the last build entirely
......@@ -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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment