1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
// Runs against the real V library, so it needs the native build on the loader
// path — `flutter test` runs on the host VM, not in the app bundle:
//
// ./tool/build.sh
// cd example && LD_LIBRARY_PATH=../build flutter test
//
// This is a smoke test of the bridge as the app uses it, not of the widgets.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vflutter_ffi/vflutter_ffi.dart' as v;
import 'package:vflutter_ffi_example/mandelbrot_dart.dart';
void main() {
_zoomTests();
const view = v.FractalView(width: 64, height: 48, maxIter: 80);
test('V fills the caller-owned buffer', () {
final pixels = v.render(view);
expect(pixels.length, view.width * view.height * 4);
// Alpha is written for every pixel, inside the set and out.
for (var i = 3; i < pixels.length; i += 4) {
expect(pixels[i], 255);
}
// The frame is not uniform — it actually contains a fractal.
expect(pixels.toSet().length, greaterThan(8));
});
test('band split matches a single-shot render', () async {
final whole = v.render(view);
final tiled = await v.renderParallel(view, tiles: 4);
expect(tiled, equals(whole));
});
test('the V and Dart kernels agree pixel-for-pixel', () {
expect(v.render(view), equals(renderDart(view)));
});
testWidgets('renders a frame computed in V', (tester) async {
await tester.pumpWidget(const MaterialApp(home: SizedBox()));
// The explorer renders asynchronously; just assert the bridge is usable
// from the test binding, which is where FFI setup usually breaks.
expect(v.add(20, 22), 42);
expect(v.greet('Flutter'), 'Hello, Flutter, from V!');
});
}
void _zoomTests() {
group('zoomedAt', () {
const view = v.FractalView(width: 800, height: 600);
test('keeps the anchor point fixed in the complex plane', () {
// The plane coordinate under a given viewport fraction must be the same
// before and after the zoom. This is the property that broke when the
// maths lived inline in the widget.
double planeX(v.FractalView f, double fx) =>
f.centerX + (fx - 0.5) * f.scale * (f.width / f.height);
double planeY(v.FractalView f, double fy) =>
f.centerY + (fy - 0.5) * f.scale;
for (final p in [0.0, 0.25, 0.5, 0.73, 1.0]) {
final zoomed = view.zoomedAt(p, p, 0.5);
expect(planeX(zoomed, p), closeTo(planeX(view, p), 1e-12));
expect(planeY(zoomed, p), closeTo(planeY(view, p), 1e-12));
}
});
test('one tap is exactly one zoom level', () {
expect(view.zoomedAt(0.5, 0.5, 0.5).scale, closeTo(view.scale / 2, 1e-12));
expect(
view.zoomedAt(0.5, 0.5, 0.5).zoomedAt(0.5, 0.5, 0.5).scale,
closeTo(view.scale / 4, 1e-12),
);
});
test('panning moves the plane under the viewport, not the other way', () {
// Dragging right must reveal what was to the LEFT, so the centre moves
// left. Getting this backwards is the classic panning bug.
final right = view.pannedBy(0.5, 0.0);
expect(right.centerX, lessThan(view.centerX));
expect(right.centerY, view.centerY);
final down = view.pannedBy(0.0, 0.5);
expect(down.centerY, lessThan(view.centerY));
expect(down.centerX, view.centerX);
});
test('a full-width drag moves exactly one viewport', () {
final panned = view.pannedBy(1.0, 0.0);
final aspect = view.width / view.height;
expect(panned.centerX,
closeTo(view.centerX - view.scale * aspect, 1e-12));
});
test('panning does not change the zoom level', () {
expect(view.pannedBy(0.3, -0.7).scale, view.scale);
});
test('panning back and forth returns to the starting view', () {
final round = view.pannedBy(0.4, 0.25).pannedBy(-0.4, -0.25);
expect(round.centerX, closeTo(view.centerX, 1e-12));
expect(round.centerY, closeTo(view.centerY, 1e-12));
});
test('pan distance scales with zoom depth', () {
// The same drag must cover less ground when zoomed in, or deep zooms
// become unnavigable.
final deep = view.copyWith(scale: view.scale / 1000);
final shallowShift = (view.pannedBy(0.5, 0).centerX - view.centerX).abs();
final deepShift = (deep.pannedBy(0.5, 0).centerX - deep.centerX).abs();
expect(deepShift, lessThan(shallowShift / 100));
});
test('zooming in then out returns to the starting view', () {
final round = view.zoomedAt(0.3, 0.8, 0.5).zoomedAt(0.3, 0.8, 2.0);
expect(round.scale, closeTo(view.scale, 1e-12));
expect(round.centerX, closeTo(view.centerX, 1e-12));
expect(round.centerY, closeTo(view.centerY, 1e-12));
});
});
}
|