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:hepan_everyday/widgets/glass_dock.dart'; import 'package:hepan_everyday/widgets/glass_refraction.dart'; void main() { testWidgets('resting cursor is neutral grey and clears while held', ( tester, ) async { final resting = await _render( tester, GlassDock(selectedIndex: 0, onSelected: (_) {}), backdropColor: Colors.white, preview: true, ); final selected = _channel(resting, 46, 52, 0); final base = _channel(resting, 160, 52, 0); expect(base - selected, greaterThan(20)); expect((_channel(resting, 46, 52, 2) - selected).abs(), lessThan(12)); final held = await _render( tester, GlassDock(selectedIndex: 0, onSelected: (_) {}), backdropColor: Colors.white, held: true, preview: true, ); expect(_channel(held, 46, 52, 0) - selected, greaterThan(18)); }); testWidgets( 'glass softens fine background detail without whitening its colour', (tester) async { final pixels = await _render( tester, const GlassRefraction(strength: 0.32, child: SizedBox.expand()), stripes: true, ); final reds = [for (var x = 110; x < 150; x++) _channel(pixels, x, 34, 0)]; final contrast = reds.reduce((a, b) => a > b ? a : b) - reds.reduce((a, b) => a < b ? a : b); expect(contrast, lessThan(25)); expect(_channel(pixels, 132, 34, 0), closeTo(130, 15)); expect(_channel(pixels, 132, 34, 1), closeTo(120, 15)); expect(_channel(pixels, 132, 34, 2), closeTo(140, 15)); expect(_channel(pixels, 132, 34, 3), 255); }, ); testWidgets( 'Dock lens preserves the backdrop colour through its neutral tint', (tester) async { final pixels = await _render( tester, GlassDock(selectedIndex: 0, onSelected: (_) {}), ); // Below the symbol and away from the rim, the selected lens should retain // this saturated cyan backdrop while applying the new resting grey tint. final red = _channel(pixels, 46, 52, 0); final green = _channel(pixels, 46, 52, 1); final blue = _channel(pixels, 46, 52, 2); expect(red, lessThan(85)); expect(green - red, greaterThan(95)); expect(blue - red, greaterThan(140)); }, ); testWidgets('Dock base and selected lens both soften the live backdrop', ( tester, ) async { final pixels = await _render( tester, GlassDock(selectedIndex: 0, onSelected: (_) {}), stripes: true, preview: true, ); for (final start in [32, 160]) { final reds = [ for (var x = start; x < start + 20; x++) _channel(pixels, x, 52, 0), ]; final contrast = reds.reduce((a, b) => a > b ? a : b) - reds.reduce((a, b) => a < b ? a : b); // Fine stripes start at 180 contrast levels; at least two thirds softens, // while the stronger curved rim can enlarge individual stripe details. expect(contrast, lessThan(60), reason: 'background at x=$start'); } }); testWidgets( 'Dock keeps larger backdrop details recognizable through its glass', (tester) async { final pixels = await _render( tester, GlassDock(selectedIndex: 0, onSelected: (_) {}), stripes: true, stripeWidth: 12, preview: true, ); for (final start in [24, 160]) { final reds = [ for (var x = start; x < start + 40; x++) _channel(pixels, x, 52, 0), ]; final contrast = reds.reduce((a, b) => a > b ? a : b) - reds.reduce((a, b) => a < b ? a : b); // The original stripes have 180 levels of contrast. Both the base and // selected lens retain over half, keeping these larger shapes readable. expect(contrast, greaterThan(90), reason: 'background at x=$start'); } }, ); testWidgets( 'composed diffusion keeps edge refraction aligned when glass moves', (tester) async { final plain = await _render( tester, const GlassRefraction(strength: 0, child: SizedBox.expand()), gradient: true, ); final refracted = await _render( tester, const GlassRefraction(strength: 1, child: SizedBox.expand()), gradient: true, ); final translated = await _render( tester, const GlassRefraction(strength: 1, child: SizedBox.expand()), gradient: true, offset: const Offset(23, -37), ); expect( (_channel(refracted, 15, 34, 0) - _channel(plain, 15, 34, 0)).abs(), greaterThan(3), ); expect( _channel(refracted, 132, 34, 0), closeTo(_channel(plain, 132, 34, 0), 2), ); for (final x in [15, 132, 248]) { expect( _channel(translated, x, 34, 0), closeTo(_channel(refracted, x, 34, 0), 2), ); } }, skip: !ui.ImageFilter.isShaderFilterSupported, ); } int _channel(ByteData pixels, int x, int y, int channel) => pixels.getUint8((y * 264 + x) * 4 + channel); Future _render( WidgetTester tester, Widget glass, { bool stripes = false, double stripeWidth = 2, bool gradient = false, bool preview = false, Offset offset = Offset.zero, Color backdropColor = const Color(0xFF14B4E6), bool held = false, }) async { tester.view.physicalSize = const Size(800, 600); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final key = GlobalKey(), glassKey = GlobalKey(); await tester.pumpWidget( RepaintBoundary( key: key, child: MaterialApp( home: Center( child: Transform.translate( offset: offset, child: SizedBox( key: glassKey, width: 264, height: 68, child: Stack( children: [ Positioned.fill( child: CustomPaint( painter: _Backdrop( stripes, gradient, stripeWidth, backdropColor, ), ), ), Positioned.fill(child: glass), ], ), ), ), ), ), ), ); if (ui.ImageFilter.isShaderFilterSupported) { // Native shader loading completes outside the widget test's fake async zone. await tester.runAsync( () => ui.FragmentProgram.fromAsset('shaders/liquid_glass.frag'), ); } await tester.pumpAndSettle(); final gesture = held ? await tester.startGesture( tester.getCenter(find.byKey(const Key('dock-cursor'))), ) : null; if (held) { await tester.pump(const Duration(milliseconds: 110)); await tester.pump(const Duration(milliseconds: 200)); } if (ui.ImageFilter.isShaderFilterSupported) { expect(find.byType(BackdropFilter), findsNothing); } final result = (await tester.runAsync(() async { final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 1); final data = (await image.toByteData(format: ui.ImageByteFormat.rawRgba))!; final region = tester.getRect(find.byKey(glassKey)); if (preview && const bool.fromEnvironment('TP_BRAND_PREVIEW')) { final recorder = ui.PictureRecorder(); ui.Canvas(recorder).drawImageRect( image, region, const Rect.fromLTWH(0, 0, 792, 204), Paint()..filterQuality = FilterQuality.high, ); final picture = recorder.endRecording(); final previewImage = await picture.toImage(792, 204); final png = (await previewImage.toByteData( format: ui.ImageByteFormat.png, ))!; final name = backdropColor == Colors.white ? held ? 'liquid-glass-held.png' : 'liquid-glass-rest.png' : 'liquid-glass-detail.png'; final output = File('../dist/screenshots/$name'); await output.parent.create(recursive: true); await output.writeAsBytes(png.buffer.asUint8List()); previewImage.dispose(); picture.dispose(); } final cropped = ByteData(264 * 68 * 4); for (var y = 0; y < 68; y++) { for (var x = 0; x < 264; x++) { final source = ((region.top.round() + y) * image.width + region.left.round() + x) * 4; for (var channel = 0; channel < 4; channel++) { cropped.setUint8( (y * 264 + x) * 4 + channel, data.getUint8(source + channel), ); } } } image.dispose(); return cropped; }))!; await gesture?.cancel(); await tester.pumpAndSettle(); return result; } class _Backdrop extends CustomPainter { const _Backdrop(this.stripes, this.gradient, this.stripeWidth, this.colour); final bool stripes; final bool gradient; final double stripeWidth; final Color colour; @override void paint(Canvas canvas, Size size) { if (gradient) { for (var x = 0; x < size.width; x++) { canvas.drawRect( Rect.fromLTWH(x.toDouble(), 0, 1, size.height), Paint()..color = Color.fromARGB(255, 40 + (x * 0.7).round(), 70, 90), ); } return; } if (!stripes) { canvas.drawRect(Offset.zero & size, Paint()..color = colour); return; } for (var x = 0.0; x < size.width; x += stripeWidth) { canvas.drawRect( Rect.fromLTWH(x, 0, stripeWidth, size.height), Paint() ..color = (x / stripeWidth).round().isEven ? const Color(0xFFDC3C3C) : const Color(0xFF28B4DC), ); } } @override bool shouldRepaint(_Backdrop oldDelegate) => oldDelegate.stripes != stripes || oldDelegate.gradient != gradient || oldDelegate.stripeWidth != stripeWidth || oldDelegate.colour != colour; }