import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ocideck/models/slide.dart'; import 'package:ocideck/widgets/slides/slide_preview.dart'; Future _writeSolidPng(String dir, String name, Color color) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.drawRect(const Rect.fromLTWH(0, 0, 8, 8), Paint()..color = color); final picture = recorder.endRecording(); final img = await picture.toImage(8, 8); final data = await img.toByteData(format: ui.ImageByteFormat.png); final file = File('$dir/$name'); file.writeAsBytesSync(data!.buffer.asUint8List()); return file; } Future<({int width, int height, Uint8List bytes})> _capture( WidgetTester tester, GlobalKey key, Widget child, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: RepaintBoundary( key: key, child: SizedBox(width: 800, height: 450, child: child), ), ), ), ), ); // Let the file images decode and paint before capturing a single frame. await Future.delayed(const Duration(milliseconds: 300)); await tester.pump(); final boundary = key.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 1.0); final bytes = (await image.toByteData( format: ui.ImageByteFormat.rawRgba, ))!.buffer.asUint8List(); return (width: image.width, height: image.height, bytes: bytes); } void main() { testWidgets('twoImages paints both the left and right images', ( tester, ) async { await tester.runAsync(() async { final dir = Directory.systemTemp.createTempSync('ocideck_two'); final red = await _writeSolidPng( dir.path, 'red.png', const Color(0xFFFF0000), ); final blue = await _writeSolidPng( dir.path, 'blue.png', const Color(0xFF0000FF), ); final slide = Slide( id: 'x', type: SlideType.twoImages, imagePath: red.path, imagePath2: blue.path, ); final key = GlobalKey(); final cap = await _capture(tester, key, SlidePreviewWidget(slide: slide)); ({int r, int g, int b}) pixelAt(double fx, double fy) { final x = (cap.width * fx).round().clamp(0, cap.width - 1); final y = (cap.height * fy).round().clamp(0, cap.height - 1); final i = (y * cap.width + x) * 4; return (r: cap.bytes[i], g: cap.bytes[i + 1], b: cap.bytes[i + 2]); } final left = pixelAt(0.25, 0.5); final right = pixelAt(0.75, 0.5); expect( left.r > 200 && left.b < 80, isTrue, reason: 'Left panel should be the red image but was $left', ); expect( right.b > 200 && right.r < 80, isTrue, reason: 'Right panel should be the blue image but was $right', ); dir.deleteSync(recursive: true); }); }); testWidgets('zoomed (scaled) image still paints in the frame', ( tester, ) async { await tester.runAsync(() async { final dir = Directory.systemTemp.createTempSync('ocideck_zoom'); final red = await _writeSolidPng( dir.path, 'red.png', const Color(0xFFFF0000), ); // imageSize 150 = zoomed in past contain; the picture must still render // (regression: the old Transform.scale rendered blank in toImage). final slide = Slide( id: 'z', type: SlideType.image, imagePath: red.path, imageSize: 150, ); final key = GlobalKey(); final cap = await _capture(tester, key, SlidePreviewWidget(slide: slide)); final cx = cap.width ~/ 2; final cy = cap.height ~/ 2; final i = (cy * cap.width + cx) * 4; final r = cap.bytes[i]; final g = cap.bytes[i + 1]; final b = cap.bytes[i + 2]; expect( r > 200 && g < 80 && b < 80, isTrue, reason: 'Center of a zoomed image should be red but was ($r,$g,$b)', ); dir.deleteSync(recursive: true); }); }); testWidgets('zoomed-out image with a title anchors to the top', ( tester, ) async { await tester.runAsync(() async { final dir = Directory.systemTemp.createTempSync('ocideck_top'); final red = await _writeSolidPng( dir.path, 'red.png', const Color(0xFFFF0000), ); // Zoomed out (60%) with a title: the image should sit at the top so the // bottom title banner does not overlap it. final slide = Slide( id: 't', type: SlideType.image, title: 'Onderschrift', imagePath: red.path, imageSize: 60, ); final key = GlobalKey(); final cap = await _capture(tester, key, SlidePreviewWidget(slide: slide)); ({int r, int g, int b}) pixelAt(double fx, double fy) { final x = (cap.width * fx).round().clamp(0, cap.width - 1); final y = (cap.height * fy).round().clamp(0, cap.height - 1); final idx = (y * cap.width + x) * 4; return ( r: cap.bytes[idx], g: cap.bytes[idx + 1], b: cap.bytes[idx + 2], ); } final top = pixelAt(0.5, 0.08); final bottom = pixelAt(0.5, 0.92); expect( top.r > 200 && top.g < 80 && top.b < 80, isTrue, reason: 'Top of a top-anchored image should be red but was $top', ); expect( bottom.r > 200 && bottom.g < 80 && bottom.b < 80, isFalse, reason: 'Bottom should be free for the title, not the image ($bottom)', ); dir.deleteSync(recursive: true); }); }); }