// 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)); }); }); }