blob: af1d2c43368a6b61e06fae825f92e1531d2a5f80 [file] [log] [blame]
// 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(),
],
),
],
);