// Copyright 2014 The Flutter 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 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; class TestCustomPainter extends CustomPainter { TestCustomPainter({ this.log, this.name }); final List<String> log; final String name; @override void paint(Canvas canvas, Size size) { log.add(name); } @override bool shouldRepaint(TestCustomPainter oldPainter) => true; } class TestCustomPainterWithCustomSemanticsBuilder extends TestCustomPainter { TestCustomPainterWithCustomSemanticsBuilder() : super(log: <String>[]); @override SemanticsBuilderCallback get semanticsBuilder => (Size size) { const Key key = Key('0'); const Rect rect = Rect.fromLTRB(0, 0, 0, 0); const SemanticsProperties semanticsProperties = SemanticsProperties(); return <CustomPainterSemantics>[ const CustomPainterSemantics(key: key, rect: rect, properties: semanticsProperties), const CustomPainterSemantics(key: key, rect: rect, properties: semanticsProperties), ]; }; } class MockCanvas extends Mock implements Canvas {} class MockPaintingContext extends Mock implements PaintingContext {} void main() { testWidgets('Control test for custom painting', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget(CustomPaint( painter: TestCustomPainter( log: log, name: 'background', ), foregroundPainter: TestCustomPainter( log: log, name: 'foreground', ), child: CustomPaint( painter: TestCustomPainter( log: log, name: 'child', ), ), )); expect(log, equals(<String>['background', 'child', 'foreground'])); }); testWidgets('Throws FlutterError on custom painter incorrect restore/save calls', ( WidgetTester tester) async { final GlobalKey target = GlobalKey(); final List<String> log = <String>[]; await tester.pumpWidget(CustomPaint( key: target, isComplex: true, painter: TestCustomPainter(log: log), )); final RenderCustomPaint renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint; final Canvas canvas = MockCanvas(); int saveCount = 0; when(canvas.getSaveCount()).thenAnswer((_) => saveCount++); final PaintingContext paintingContext = MockPaintingContext(); when(paintingContext.canvas).thenReturn(canvas); FlutterError getError() { FlutterError error; try { renderCustom.paint(paintingContext, const Offset(0, 0)); } on FlutterError catch (e) { error = e; } return error; } FlutterError error = getError(); expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The TestCustomPainter#00000() custom painter called canvas.save()\n' ' or canvas.saveLayer() at least 1 more time than it called\n' ' canvas.restore().\n' ' This leaves the canvas in an inconsistent state and will probably\n' ' result in a broken display.\n' ' You must pair each call to save()/saveLayer() with a later\n' ' matching call to restore().\n' )); when(canvas.getSaveCount()).thenAnswer((_) => saveCount--); error = getError(); expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The TestCustomPainter#00000() custom painter called\n' ' canvas.restore() 1 more time than it called canvas.save() or\n' ' canvas.saveLayer().\n' ' This leaves the canvas in an inconsistent state and will result\n' ' in a broken display.\n' ' You should only call restore() if you first called save() or\n' ' saveLayer().\n' )); when(canvas.getSaveCount()).thenAnswer((_) => saveCount += 2); error = getError(); expect(error.toStringDeep(), contains('2 more times')); when(canvas.getSaveCount()).thenAnswer((_) => saveCount -= 2); error = getError(); expect(error.toStringDeep(), contains('2 more times')); }); testWidgets('assembleSemanticsNode throws FlutterError', (WidgetTester tester) async { final List<String> log = <String>[]; final GlobalKey target = GlobalKey(); await tester.pumpWidget(CustomPaint( key: target, isComplex: true, painter: TestCustomPainter(log: log), )); final RenderCustomPaint renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint; dynamic error; try { renderCustom.assembleSemanticsNode( null, null, <SemanticsNode>[SemanticsNode()], ); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' RenderCustomPaint does not have a child widget but received a\n' ' non-empty list of child SemanticsNode:\n' ' SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible)\n' )); await tester.pumpWidget(CustomPaint( key: target, isComplex: true, painter: TestCustomPainterWithCustomSemanticsBuilder(), )); final dynamic exception = tester.takeException(); expect(exception, isFlutterError); error = exception; expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' Failed to update the list of CustomPainterSemantics:\n' " - duplicate key [<'0'>] found at position 1\n" )); }); testWidgets('CustomPaint sizing', (WidgetTester tester) async { final GlobalKey target = GlobalKey(); await tester.pumpWidget(Center( child: CustomPaint(key: target), )); expect(target.currentContext.size, Size.zero); await tester.pumpWidget(Center( child: CustomPaint(key: target, child: Container()), )); expect(target.currentContext.size, const Size(800.0, 600.0)); await tester.pumpWidget(Center( child: CustomPaint(key: target, size: const Size(20.0, 20.0)), )); expect(target.currentContext.size, const Size(20.0, 20.0)); await tester.pumpWidget(Center( child: CustomPaint(key: target, size: const Size(2000.0, 100.0)), )); expect(target.currentContext.size, const Size(800.0, 100.0)); await tester.pumpWidget(Center( child: CustomPaint(key: target, size: Size.zero, child: Container()), )); expect(target.currentContext.size, const Size(800.0, 600.0)); await tester.pumpWidget(Center( child: CustomPaint(key: target, child: const SizedBox(height: 0.0, width: 0.0)), )); expect(target.currentContext.size, Size.zero); }); testWidgets('Raster cache hints', (WidgetTester tester) async { final GlobalKey target = GlobalKey(); final List<String> log = <String>[]; await tester.pumpWidget(CustomPaint( key: target, isComplex: true, painter: TestCustomPainter(log: log), )); RenderCustomPaint renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint; expect(renderCustom.isComplex, true); expect(renderCustom.willChange, false); await tester.pumpWidget(CustomPaint( key: target, willChange: true, foregroundPainter: TestCustomPainter(log: log), )); renderCustom = target.currentContext.findRenderObject() as RenderCustomPaint; expect(renderCustom.isComplex, false); expect(renderCustom.willChange, true); }); test('Raster cache hints cannot be set with null painters', () { expect(() => CustomPaint(isComplex: true), throwsAssertionError); expect(() => CustomPaint(willChange: true), throwsAssertionError); }); }