// 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. // ignore_for_file: public_member_api_docs // This is the test for the private implementation of animated icons. // To make the private API accessible from the test we do not import the // material material_animated_icons library, but instead, this test file is an // implementation of that library, using some of the parts of the real // material_animated_icons, this give the test access to the private APIs. library material_animated_icons; import 'dart:math' as math show pi; import 'dart:ui' show lerpDouble, Offset; import 'dart:ui' as ui show Paint, Path, Canvas; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; part 'src/material/animated_icons/animated_icons.dart'; part 'src/material/animated_icons/animated_icons_data.dart'; // We have to import all the generated files in the material library to avoid // analysis errors (as the generated constants are all referenced in the // animated_icons library). part 'src/material/animated_icons/data/add_event.g.dart'; part 'src/material/animated_icons/data/arrow_menu.g.dart'; part 'src/material/animated_icons/data/close_menu.g.dart'; part 'src/material/animated_icons/data/ellipsis_search.g.dart'; part 'src/material/animated_icons/data/event_add.g.dart'; part 'src/material/animated_icons/data/home_menu.g.dart'; part 'src/material/animated_icons/data/list_view.g.dart'; part 'src/material/animated_icons/data/menu_arrow.g.dart'; part 'src/material/animated_icons/data/menu_close.g.dart'; part 'src/material/animated_icons/data/menu_home.g.dart'; part 'src/material/animated_icons/data/pause_play.g.dart'; part 'src/material/animated_icons/data/play_pause.g.dart'; part 'src/material/animated_icons/data/search_ellipsis.g.dart'; part 'src/material/animated_icons/data/view_list.g.dart'; class MockCanvas extends Mock implements Canvas {} class MockPath extends Mock implements Path {} void main() { group('Interpolate points', () { test('- single point', () { const List<Offset> points = <Offset>[ Offset(25.0, 1.0), ]; expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0)); expect(_interpolate(points, 0.5, Offset.lerp), const Offset(25.0, 1.0)); expect(_interpolate(points, 1.0, Offset.lerp), const Offset(25.0, 1.0)); }); test('- two points', () { const List<Offset> points = <Offset>[ Offset(25.0, 1.0), Offset(12.0, 12.0), ]; expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0)); expect(_interpolate(points, 0.5, Offset.lerp), const Offset(18.5, 6.5)); expect(_interpolate(points, 1.0, Offset.lerp), const Offset(12.0, 12.0)); }); test('- three points', () { const List<Offset> points = <Offset>[ Offset(25.0, 1.0), Offset(12.0, 12.0), Offset(23.0, 9.0), ]; expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0)); expect(_interpolate(points, 0.25, Offset.lerp), const Offset(18.5, 6.5)); expect(_interpolate(points, 0.5, Offset.lerp), const Offset(12.0, 12.0)); expect(_interpolate(points, 0.75, Offset.lerp), const Offset(17.5, 10.5)); expect(_interpolate(points, 1.0, Offset.lerp), const Offset(23.0, 9.0)); }); }); group('_AnimatedIconPainter', () { const Size size = Size(48.0, 48.0); late MockPath mockPath; late MockCanvas mockCanvas; late List<MockPath> generatedPaths; late _UiPathFactory pathFactory; setUp(() { generatedPaths = <MockPath>[]; mockCanvas = MockCanvas(); mockPath = MockPath(); pathFactory = () { generatedPaths.add(mockPath); return mockPath; }; }); test('progress 0', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _movingBar.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); expect(generatedPaths.length, 1); generatedPaths[0].verifyCallsInOrder(<MockCall>[ MockCall('moveTo', <dynamic>[0.0, 0.0]), MockCall('lineTo', <dynamic>[48.0, 0.0]), MockCall('lineTo', <dynamic>[48.0, 10.0]), MockCall('lineTo', <dynamic>[0.0, 10.0]), MockCall('lineTo', <dynamic>[0.0, 0.0]), MockCall('close'), ]); }); test('progress 1', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _movingBar.paths, progress: const AlwaysStoppedAnimation<double>(1.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); expect(generatedPaths.length, 1); generatedPaths[0].verifyCallsInOrder(<MockCall>[ MockCall('moveTo', <dynamic>[0.0, 38.0]), MockCall('lineTo', <dynamic>[48.0, 38.0]), MockCall('lineTo', <dynamic>[48.0, 48.0]), MockCall('lineTo', <dynamic>[0.0, 48.0]), MockCall('lineTo', <dynamic>[0.0, 38.0]), MockCall('close'), ]); }); test('clamped progress', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _movingBar.paths, progress: const AlwaysStoppedAnimation<double>(1.5), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); expect(generatedPaths.length, 1); generatedPaths[0].verifyCallsInOrder(<MockCall>[ MockCall('moveTo', <dynamic>[0.0, 38.0]), MockCall('lineTo', <dynamic>[48.0, 38.0]), MockCall('lineTo', <dynamic>[48.0, 48.0]), MockCall('lineTo', <dynamic>[0.0, 48.0]), MockCall('lineTo', <dynamic>[0.0, 38.0]), MockCall('close'), ]); }); test('scale', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _movingBar.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 0.5, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); mockCanvas.verifyCallsInOrder(<MockCall>[ MockCall('scale', <dynamic>[0.5, 0.5]), MockCall.any('drawPath'), ]); }); test('mirror', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _movingBar.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: true, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); mockCanvas.verifyCallsInOrder(<MockCall>[ MockCall('scale', <dynamic>[1.0, 1.0]), MockCall('rotate', <dynamic>[math.pi]), MockCall('translate', <dynamic>[-48.0, -48.0]), MockCall.any('drawPath'), ]); }); test('interpolated frame', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _movingBar.paths, progress: const AlwaysStoppedAnimation<double>(0.5), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); expect(generatedPaths.length, 1); generatedPaths[0].verifyCallsInOrder(<MockCall>[ MockCall('moveTo', <dynamic>[0.0, 19.0]), MockCall('lineTo', <dynamic>[48.0, 19.0]), MockCall('lineTo', <dynamic>[48.0, 29.0]), MockCall('lineTo', <dynamic>[0.0, 29.0]), MockCall('lineTo', <dynamic>[0.0, 19.0]), MockCall('close'), ]); }); test('curved frame', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(1.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); expect(generatedPaths.length, 1); generatedPaths[0].verifyCallsInOrder(<MockCall>[ MockCall('moveTo', <dynamic>[0.0, 24.0]), MockCall('cubicTo', <dynamic>[16.0, 48.0, 32.0, 48.0, 48.0, 24.0]), MockCall('lineTo', <dynamic>[0.0, 24.0]), MockCall('close'), ]); }); test('interpolated curved frame', () { final _AnimatedIconPainter painter = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.25), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); painter.paint(mockCanvas, size); expect(generatedPaths.length, 1); generatedPaths[0].verifyCallsInOrder(<MockCall>[ MockCall('moveTo', <dynamic>[0.0, 24.0]), MockCall('cubicTo', <dynamic>[16.0, 17.0, 32.0, 17.0, 48.0, 24.0]), MockCall('lineTo', <dynamic>[0.0, 24.0]), MockCall('close', <dynamic>[]), ]); }); test('should not repaint same values', () { final _AnimatedIconPainter painter1 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); final _AnimatedIconPainter painter2 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); expect(painter1.shouldRepaint(painter2), false); }); test('should repaint on progress change', () { final _AnimatedIconPainter painter1 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); final _AnimatedIconPainter painter2 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.1), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); expect(painter1.shouldRepaint(painter2), true); }); test('should repaint on color change', () { final _AnimatedIconPainter painter1 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF00FF00), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); final _AnimatedIconPainter painter2 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFFFF0000), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); expect(painter1.shouldRepaint(painter2), true); }); test('should repaint on paths change', () { final _AnimatedIconPainter painter1 = _AnimatedIconPainter( paths: _bow.paths, progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF0000FF), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); final _AnimatedIconPainter painter2 = _AnimatedIconPainter( paths: const <_PathFrames>[], progress: const AlwaysStoppedAnimation<double>(0.0), color: const Color(0xFF0000FF), scale: 1.0, shouldMirror: false, uiPathFactory: pathFactory, ); expect(painter1.shouldRepaint(painter2), true); }); }); } // Contains the data from an invocation used for collection of calls and for // expectations in Mock class. class MockCall { // Creates a mock call with optional positional arguments. MockCall(String memberName, [this.positionalArguments, this.acceptAny = false]) : memberSymbol = Symbol(memberName); MockCall.fromSymbol(this.memberSymbol, [this.positionalArguments, this.acceptAny = false]); // Creates a mock call expectation that doesn't care about what the arguments were. MockCall.any(String memberName) : memberSymbol = Symbol(memberName), acceptAny = true, positionalArguments = null; final Symbol memberSymbol; String get memberName { final RegExp symbolMatch = RegExp(r'Symbol\("(?<name>.*)"\)'); final RegExpMatch? match = symbolMatch.firstMatch(memberSymbol.toString()); assert(match != null); return match!.namedGroup('name')!; } final List<dynamic>? positionalArguments; final bool acceptAny; @override String toString() { return '$memberName(${positionalArguments?.join(', ') ?? ''})'; } } // A very simplified version of a Mock class. // // Only verifies positional arguments, and only can verify calls in order. class Mock { final List<MockCall> _calls = <MockCall>[]; // Verify that the given calls happened in the order given. void verifyCallsInOrder(List<MockCall> expected) { int count = 0; expect(expected.length, equals(_calls.length), reason: 'Incorrect number of calls received. ' 'Expected ${expected.length} and received ${_calls.length}.\n' ' Calls Received: $_calls\n' ' Calls Expected: $expected'); for (final MockCall call in _calls) { expect(call.memberSymbol, equals(expected[count].memberSymbol), reason: 'Unexpected call to ${call.memberName}, expected a call to ' '${expected[count].memberName} instead.'); if (call.positionalArguments != null && !expected[count].acceptAny) { int countArg = 0; for (final dynamic arg in call.positionalArguments!) { expect(arg, equals(expected[count].positionalArguments![countArg]), reason: 'Failed at call $count. Positional argument $countArg to ${call.memberName} ' 'not as expected. Expected ${expected[count].positionalArguments![countArg]} ' 'and received $arg'); countArg++; } } count++; } } @override void noSuchMethod(Invocation invocation) { _calls.add(MockCall.fromSymbol(invocation.memberName, invocation.positionalArguments)); } } const _AnimatedIconData _movingBar = _AnimatedIconData( Size(48.0, 48.0), <_PathFrames>[ _PathFrames( opacities: <double>[1.0, 0.2], commands: <_PathCommand>[ _PathMoveTo( <Offset>[ Offset.zero, Offset(0.0, 38.0), ], ), _PathLineTo( <Offset>[ Offset(48.0, 0.0), Offset(48.0, 38.0), ], ), _PathLineTo( <Offset>[ Offset(48.0, 10.0), Offset(48.0, 48.0), ], ), _PathLineTo( <Offset>[ Offset(0.0, 10.0), Offset(0.0, 48.0), ], ), _PathLineTo( <Offset>[ Offset.zero, Offset(0.0, 38.0), ], ), _PathClose(), ], ), ], ); const _AnimatedIconData _bow = _AnimatedIconData( Size(48.0, 48.0), <_PathFrames>[ _PathFrames( opacities: <double>[1.0, 1.0], commands: <_PathCommand>[ _PathMoveTo( <Offset>[ Offset(0.0, 24.0), Offset(0.0, 24.0), Offset(0.0, 24.0), ], ), _PathCubicTo( <Offset>[ Offset(16.0, 24.0), Offset(16.0, 10.0), Offset(16.0, 48.0), ], <Offset>[ Offset(32.0, 24.0), Offset(32.0, 10.0), Offset(32.0, 48.0), ], <Offset>[ Offset(48.0, 24.0), Offset(48.0, 24.0), Offset(48.0, 24.0), ], ), _PathLineTo( <Offset>[ Offset(0.0, 24.0), Offset(0.0, 24.0), Offset(0.0, 24.0), ], ), _PathClose(), ], ), ], );