| // 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 Offset, lerpDouble; |
| import 'dart:ui' as ui show Canvas, Paint, Path; |
| |
| import 'package:flutter/foundation.dart' show clampDouble; |
| 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('rotate', <dynamic>[math.pi]), |
| MockCall('translate', <dynamic>[-48.0, -48.0]), |
| MockCall('scale', <dynamic>[1.0, 1.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(), |
| ], |
| ), |
| ], |
| ); |