// 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); }