| // 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 'dart:math'; |
| |
| import 'package:collection/collection.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:vitool/vitool.dart'; |
| |
| void main() { |
| |
| test('parsePixels', () { |
| expect(parsePixels('23px'), 23); |
| expect(parsePixels('9px'), 9); |
| expect(() { parsePixels('9pt'); }, throwsArgumentError); |
| }); |
| |
| test('parsePoints', () { |
| expect(parsePoints('1.0, 2.0'), |
| const <Point<double>>[Point<double>(1.0, 2.0)], |
| ); |
| expect(parsePoints('12.0, 34.0 5.0, 6.6'), |
| const <Point<double>>[ |
| Point<double>(12.0, 34.0), |
| Point<double>(5.0, 6.6), |
| ], |
| ); |
| expect(parsePoints('12.0 34.0 5.0 6.6'), |
| const <Point<double>>[ |
| Point<double>(12.0, 34.0), |
| Point<double>(5.0, 6.6), |
| ], |
| ); |
| }); |
| |
| group('parseSvg', () { |
| test('empty SVGs', () { |
| interpretSvg(testAsset('empty_svg_1_48x48.svg')); |
| interpretSvg(testAsset('empty_svg_2_100x50.svg')); |
| }); |
| |
| test('illegal SVGs', () { |
| expect( |
| () { interpretSvg(testAsset('illegal_svg_multiple_roots.svg')); }, |
| throwsA(anything), |
| ); |
| }); |
| |
| test('SVG size', () { |
| expect( |
| interpretSvg(testAsset('empty_svg_1_48x48.svg')).size, |
| const Point<double>(48.0, 48.0), |
| ); |
| |
| expect( |
| interpretSvg(testAsset('empty_svg_2_100x50.svg')).size, |
| const Point<double>(100.0, 50.0), |
| ); |
| }); |
| |
| test('horizontal bar', () { |
| final FrameData frameData = interpretSvg(testAsset('horizontal_bar.svg')); |
| expect(frameData.paths, <SvgPath>[ |
| const SvgPath('path_1', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| ]); |
| }); |
| |
| test('leading space path command', () { |
| interpretSvg(testAsset('leading_space_path_command.svg')); |
| }); |
| |
| test('SVG illegal path', () { |
| expect( |
| () { interpretSvg(testAsset('illegal_path.svg')); }, |
| throwsA(anything), |
| ); |
| }); |
| |
| |
| test('SVG group', () { |
| final FrameData frameData = interpretSvg(testAsset('bars_group.svg')); |
| expect(frameData.paths, const <SvgPath>[ |
| SvgPath('path_1', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| SvgPath('path_2', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 34.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 34.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 44.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 44.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| ]); |
| }); |
| |
| test('SVG group translate', () { |
| final FrameData frameData = interpretSvg(testAsset('bar_group_translate.svg')); |
| expect(frameData.paths, const <SvgPath>[ |
| SvgPath('path_1', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 34.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 34.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 44.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 44.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| ]); |
| }); |
| |
| test('SVG group scale', () { |
| final FrameData frameData = interpretSvg(testAsset('bar_group_scale.svg')); |
| expect(frameData.paths, const <SvgPath>[ |
| SvgPath( |
| 'path_1', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 9.5)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(24.0, 9.5)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(24.0, 14.5)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 14.5)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| ]); |
| }); |
| |
| test('SVG group rotate scale', () { |
| final FrameData frameData = interpretSvg(testAsset('bar_group_rotate_scale.svg')); |
| expect(frameData.paths, const <PathMatcher>[ |
| PathMatcher( |
| SvgPath( |
| 'path_1', <SvgPathCommand>[ |
| SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 0.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 48.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(19.0, 48.0)]), |
| SvgPathCommand('M', <Point<double>>[Point<double>(19.0, 0.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| margin: precisionErrorTolerance, |
| ), |
| ]); |
| }); |
| |
| test('SVG illegal transform', () { |
| expect( |
| () { interpretSvg(testAsset('illegal_transform.svg')); }, |
| throwsA(anything), |
| ); |
| }); |
| |
| test('SVG group opacity', () { |
| final FrameData frameData = interpretSvg(testAsset('bar_group_opacity.svg')); |
| expect(frameData.paths, const <SvgPath>[ |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ], |
| opacity: 0.5, |
| ), |
| ]); |
| }); |
| |
| test('horizontal bar relative', () { |
| // This asset uses the relative 'l' command instead of 'L'. |
| final FrameData frameData = interpretSvg(testAsset('horizontal_bar_relative.svg')); |
| expect(frameData.paths, const <SvgPath>[ |
| SvgPath( |
| 'path_1', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| ]); |
| }); |
| |
| test('close in middle of path', () { |
| // This asset uses the relative 'l' command instead of 'L'. |
| final FrameData frameData = interpretSvg(testAsset('close_path_in_middle.svg')); |
| expect(frameData.paths, const <SvgPath>[ |
| SvgPath( |
| 'path_1', <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(50.0, 50.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(60.0, 50.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(60.0, 60.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(50.0, 40.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(40.0, 40.0)]), |
| SvgPathCommand('Z', <Point<double>>[]), |
| ]), |
| ]); |
| }); |
| }); |
| |
| group('create PathAnimation', () { |
| test('single path', () { |
| const List<FrameData> frameData = <FrameData>[ |
| FrameData( |
| Point<double>(10.0, 10.0), |
| <SvgPath>[ |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]), |
| SvgPathCommand('L', <Point<double>>[Point<double>(10.0, 10.0)]), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| expect(PathAnimation.fromFrameData(frameData, 0), |
| const PathAnimationMatcher(PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[Point<double>(0.0, 0.0)], |
| ]), |
| PathCommandAnimation('L', <List<Point<double>>>[ |
| <Point<double>>[Point<double>(10.0, 10.0)], |
| ]), |
| ], |
| opacities: <double>[1.0], |
| )), |
| ); |
| }); |
| |
| test('multiple paths', () { |
| const List<FrameData> frameData = <FrameData>[ |
| FrameData( |
| Point<double>(10.0, 10.0), |
| <SvgPath>[ |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]), |
| ], |
| ), |
| SvgPath( |
| 'path_2', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| expect(PathAnimation.fromFrameData(frameData, 0), |
| const PathAnimationMatcher(PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[Point<double>(0.0, 0.0)], |
| ]), |
| ], |
| opacities: <double>[1.0], |
| )), |
| ); |
| |
| expect(PathAnimation.fromFrameData(frameData, 1), |
| const PathAnimationMatcher(PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[Point<double>(5.0, 6.0)], |
| ]) |
| ], |
| opacities: <double>[1.0], |
| )), |
| ); |
| }); |
| |
| test('multiple frames', () { |
| const List<FrameData> frameData = <FrameData>[ |
| FrameData( |
| Point<double>(10.0, 10.0), |
| <SvgPath>[ |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]), |
| ], |
| opacity: 0.5, |
| ), |
| ], |
| ), |
| FrameData( |
| Point<double>(10.0, 10.0), |
| <SvgPath>[ |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(10.0, 10.0)]), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| expect(PathAnimation.fromFrameData(frameData, 0), |
| const PathAnimationMatcher(PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(0.0, 0.0), |
| Point<double>(10.0, 10.0), |
| ], |
| ]), |
| ], |
| opacities: <double>[0.5, 1.0], |
| )), |
| ); |
| }); |
| }); |
| |
| group('create Animation', () { |
| test('multiple paths', () { |
| const List<FrameData> frameData = <FrameData>[ |
| FrameData( |
| Point<double>(10.0, 10.0), |
| <SvgPath>[ |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]), |
| ], |
| ), |
| SvgPath( |
| 'path_1', |
| <SvgPathCommand>[ |
| SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]), |
| ], |
| ), |
| ], |
| ), |
| ]; |
| final Animation animation = Animation.fromFrameData(frameData); |
| expect(animation.paths[0], |
| const PathAnimationMatcher(PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[Point<double>(0.0, 0.0)], |
| ]), |
| ], |
| opacities: <double>[1.0], |
| )), |
| ); |
| |
| expect(animation.paths[1], |
| const PathAnimationMatcher(PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[Point<double>(5.0, 6.0)], |
| ]), |
| ], |
| opacities: <double>[1.0], |
| )), |
| ); |
| |
| expect(animation.size, const Point<double>(10.0, 10.0)); |
| }); |
| }); |
| |
| group('toDart', () { |
| test('_PathMoveTo', () { |
| const PathCommandAnimation command = PathCommandAnimation( |
| 'M', |
| <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(1.0, 2.0), |
| Point<double>(3.0, 4.0), |
| ], |
| ], |
| ); |
| |
| expect(command.toDart(), |
| ' const _PathMoveTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(1.0, 2.0),\n' |
| ' const Offset(3.0, 4.0),\n' |
| ' ],\n' |
| ' ),\n', |
| |
| ); |
| }); |
| |
| test('_PathLineTo', () { |
| const PathCommandAnimation command = PathCommandAnimation( |
| 'L', |
| <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(1.0, 2.0), |
| Point<double>(3.0, 4.0), |
| ], |
| ], |
| ); |
| |
| expect(command.toDart(), |
| ' const _PathLineTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(1.0, 2.0),\n' |
| ' const Offset(3.0, 4.0),\n' |
| ' ],\n' |
| ' ),\n', |
| |
| ); |
| }); |
| |
| test('_PathCubicTo', () { |
| const PathCommandAnimation command = PathCommandAnimation( |
| 'C', |
| <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(16.0, 24.0), |
| Point<double>(16.0, 10.0), |
| ], |
| <Point<double>>[ |
| Point<double>(16.0, 25.0), |
| Point<double>(16.0, 11.0), |
| ], |
| <Point<double>>[ |
| Point<double>(40.0, 40.0), |
| Point<double>(40.0, 40.0), |
| ], |
| ], |
| ); |
| |
| expect(command.toDart(), |
| ' const _PathCubicTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(16.0, 24.0),\n' |
| ' const Offset(16.0, 10.0),\n' |
| ' ],\n' |
| ' const <Offset>[\n' |
| ' const Offset(16.0, 25.0),\n' |
| ' const Offset(16.0, 11.0),\n' |
| ' ],\n' |
| ' const <Offset>[\n' |
| ' const Offset(40.0, 40.0),\n' |
| ' const Offset(40.0, 40.0),\n' |
| ' ],\n' |
| ' ),\n', |
| |
| ); |
| }); |
| |
| test('_PathClose', () { |
| const PathCommandAnimation command = PathCommandAnimation( |
| 'Z', |
| <List<Point<double>>>[], |
| ); |
| |
| expect(command.toDart(), |
| ' const _PathClose(\n' |
| ' ),\n', |
| |
| ); |
| }); |
| |
| test('Unsupported path command', () { |
| const PathCommandAnimation command = PathCommandAnimation( |
| 'h', |
| <List<Point<double>>>[], |
| ); |
| |
| expect( |
| () { command.toDart(); }, |
| throwsA(anything), |
| ); |
| }); |
| |
| test('_PathFrames', () { |
| const PathAnimation pathAnimation = PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(0.0, 0.0), |
| Point<double>(10.0, 10.0), |
| ], |
| ]), |
| PathCommandAnimation('L', <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(48.0, 10.0), |
| Point<double>(0.0, 0.0), |
| ], |
| ]), |
| ], |
| opacities: <double>[0.5, 1.0], |
| ); |
| |
| expect(pathAnimation.toDart(), |
| ' const _PathFrames(\n' |
| ' opacities: const <double>[\n' |
| ' 0.5,\n' |
| ' 1.0,\n' |
| ' ],\n' |
| ' commands: const <_PathCommand>[\n' |
| ' const _PathMoveTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(0.0, 0.0),\n' |
| ' const Offset(10.0, 10.0),\n' |
| ' ],\n' |
| ' ),\n' |
| ' const _PathLineTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(48.0, 10.0),\n' |
| ' const Offset(0.0, 0.0),\n' |
| ' ],\n' |
| ' ),\n' |
| ' ],\n' |
| ' ),\n', |
| ); |
| }); |
| |
| test('Animation', () { |
| const Animation animation = Animation( |
| Point<double>(48.0, 48.0), |
| <PathAnimation>[ |
| PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(0.0, 0.0), |
| Point<double>(10.0, 10.0), |
| ], |
| ]), |
| PathCommandAnimation('L', <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(48.0, 10.0), |
| Point<double>(0.0, 0.0), |
| ], |
| ]), |
| ], |
| opacities: <double>[0.5, 1.0], |
| ), |
| |
| PathAnimation( |
| <PathCommandAnimation>[ |
| PathCommandAnimation('M', <List<Point<double>>>[ |
| <Point<double>>[ |
| Point<double>(0.0, 0.0), |
| Point<double>(10.0, 10.0), |
| ], |
| ]), |
| ], |
| opacities: <double>[0.5, 1.0], |
| ), |
| ]); |
| |
| expect(animation.toDart('_AnimatedIconData', r'_$data1'), |
| 'const _AnimatedIconData _\$data1 = const _AnimatedIconData(\n' |
| ' const Size(48.0, 48.0),\n' |
| ' const <_PathFrames>[\n' |
| ' const _PathFrames(\n' |
| ' opacities: const <double>[\n' |
| ' 0.5,\n' |
| ' 1.0,\n' |
| ' ],\n' |
| ' commands: const <_PathCommand>[\n' |
| ' const _PathMoveTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(0.0, 0.0),\n' |
| ' const Offset(10.0, 10.0),\n' |
| ' ],\n' |
| ' ),\n' |
| ' const _PathLineTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(48.0, 10.0),\n' |
| ' const Offset(0.0, 0.0),\n' |
| ' ],\n' |
| ' ),\n' |
| ' ],\n' |
| ' ),\n' |
| ' const _PathFrames(\n' |
| ' opacities: const <double>[\n' |
| ' 0.5,\n' |
| ' 1.0,\n' |
| ' ],\n' |
| ' commands: const <_PathCommand>[\n' |
| ' const _PathMoveTo(\n' |
| ' const <Offset>[\n' |
| ' const Offset(0.0, 0.0),\n' |
| ' const Offset(10.0, 10.0),\n' |
| ' ],\n' |
| ' ),\n' |
| ' ],\n' |
| ' ),\n' |
| ' ],\n' |
| ');', |
| ); |
| }); |
| }); |
| } |
| |
| // Matches all path commands' points within an error margin. |
| class PathMatcher extends Matcher { |
| const PathMatcher(this.actual, {this.margin = 0.0}); |
| |
| final SvgPath actual; |
| final double margin; |
| |
| @override |
| Description describe(Description description) => description.add('$actual (±$margin)'); |
| |
| @override |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { |
| if (item == null || actual == null) |
| return item == actual; |
| |
| if (item.runtimeType != actual.runtimeType) |
| return false; |
| |
| final SvgPath other = item as SvgPath; |
| if (other.id != actual.id || other.opacity != actual.opacity) |
| return false; |
| |
| if (other.commands.length != actual.commands.length) |
| return false; |
| |
| for (int i = 0; i < other.commands.length; i += 1) { |
| if (!commandsMatch(actual.commands[i], other.commands[i])) |
| return false; |
| } |
| return true; |
| } |
| |
| bool commandsMatch(SvgPathCommand actual, SvgPathCommand other) { |
| if (other.points.length != actual.points.length) |
| return false; |
| |
| for (int i = 0; i < other.points.length; i += 1) { |
| if ((other.points[i].x - actual.points[i].x).abs() > margin) |
| return false; |
| if ((other.points[i].y - actual.points[i].y).abs() > margin) |
| return false; |
| } |
| return true; |
| } |
| } |
| |
| class PathAnimationMatcher extends Matcher { |
| const PathAnimationMatcher(this.expected); |
| |
| final PathAnimation expected; |
| |
| @override |
| Description describe(Description description) => description.add('$expected'); |
| |
| @override |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { |
| if (item == null || expected == null) |
| return item == expected; |
| |
| if (item.runtimeType != expected.runtimeType) |
| return false; |
| |
| final PathAnimation other = item as PathAnimation; |
| |
| if (!const ListEquality<double>().equals(other.opacities, expected.opacities)) |
| return false; |
| |
| if (other.commands.length != expected.commands.length) |
| return false; |
| |
| for (int i = 0; i < other.commands.length; i += 1) { |
| if (!commandsMatch(expected.commands[i], other.commands[i])) |
| return false; |
| } |
| return true; |
| } |
| |
| bool commandsMatch(PathCommandAnimation expected, PathCommandAnimation other) { |
| if (other.points.length != expected.points.length) |
| return false; |
| |
| for (int i = 0; i < other.points.length; i += 1) |
| if (!const ListEquality<Point<double>>().equals(other.points[i], expected.points[i])) |
| return false; |
| |
| return true; |
| } |
| } |
| |
| String testAsset(String name) { |
| return path.join('test_assets', name); |
| } |