blob: a5491e9c1bdd04e9fa3077b3a673793ccd5870ff [file] [log] [blame] [edit]
// 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);
}