Skip to content

Commit d13eb9e

Browse files
committed
Adding tests for SVG and fixing height/width bug
- Reduced code by putting attributes in a local - Add { } around if's to make code friendlier to code coverage - Add more unit tests to get to 57.4% - Fix height/width bug in svgTagRender (using width for height) - Add height/width to the svgDataImagerRender for Picture.string - Refactored unit tests to simplify and reduce code - Add coverage and scratch directories to .gitignore
1 parent c75e0df commit d13eb9e

7 files changed

+301
-164
lines changed

packages/flutter_html_svg/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
.pub-cache/
3030
.pub/
3131
build/
32+
coverage/
33+
scratch/
3234

3335
# Android related
3436
**/android/**/gradle-wrapper.jar

packages/flutter_html_svg/lib/flutter_html_svg.dart

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
library flutter_html_svg;
22

33
import 'dart:convert';
4-
54
import 'package:flutter/material.dart';
65
import 'package:flutter_html/flutter_html.dart';
76
// ignore: implementation_imports
@@ -11,16 +10,18 @@ import 'package:flutter_svg/flutter_svg.dart';
1110
/// The CustomRender function that renders the <svg> HTML tag.
1211
CustomRender svgTagRender() =>
1312
CustomRender.widget(widget: (context, buildChildren) {
13+
final attributes =
14+
context.tree.element?.attributes.cast<String, String>() ??
15+
<String, String>{};
16+
1417
return Builder(
1518
key: context.key,
1619
builder: (buildContext) {
1720
return GestureDetector(
1821
child: SvgPicture.string(
1922
context.tree.element?.outerHtml ?? "",
20-
width: double.tryParse(
21-
context.tree.element?.attributes['width'] ?? ""),
22-
height: double.tryParse(
23-
context.tree.element?.attributes['width'] ?? ""),
23+
width: _width(attributes),
24+
height: _height(attributes),
2425
),
2526
onTap: () {
2627
if (MultipleTapGestureDetector.of(buildContext) != null) {
@@ -29,7 +30,7 @@ CustomRender svgTagRender() =>
2930
context.parser.onImageTap?.call(
3031
context.tree.element?.outerHtml ?? "",
3132
context,
32-
context.tree.element!.attributes.cast(),
33+
attributes,
3334
context.tree.element);
3435
},
3536
);
@@ -39,32 +40,39 @@ CustomRender svgTagRender() =>
3940
/// The CustomRender function that renders an <img> tag with hardcoded svg data.
4041
CustomRender svgDataImageRender() =>
4142
CustomRender.widget(widget: (context, buildChildren) {
42-
final dataUri = _dataUriFormat.firstMatch(
43-
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!);
43+
final attributes =
44+
context.tree.element?.attributes.cast<String, String>() ??
45+
<String, String>{};
46+
final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
4447
final data = dataUri?.namedGroup('data');
45-
if (data == null) return const SizedBox(height: 0, width: 0);
48+
49+
if (data == null || data.isEmpty) {
50+
return const SizedBox(height: 0, width: 0);
51+
}
4652
return Builder(
4753
key: context.key,
4854
builder: (buildContext) {
55+
final width = _width(attributes);
56+
final height = _height(attributes);
57+
4958
return GestureDetector(
5059
child: dataUri?.namedGroup('encoding') == ';base64'
5160
? SvgPicture.memory(
5261
base64.decode(data.trim()),
53-
width: _width(context.tree.element?.attributes.cast() ??
54-
<String, String>{}),
55-
height: _height(context.tree.element?.attributes.cast() ??
56-
<String, String>{}),
62+
width: width,
63+
height: height,
5764
)
58-
: SvgPicture.string(Uri.decodeFull(data)),
65+
: SvgPicture.string(
66+
Uri.decodeFull(data),
67+
width: width,
68+
height: height,
69+
),
5970
onTap: () {
6071
if (MultipleTapGestureDetector.of(buildContext) != null) {
6172
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
6273
}
63-
context.parser.onImageTap?.call(
64-
Uri.decodeFull(data),
65-
context,
66-
context.tree.element!.attributes.cast(),
67-
context.tree.element);
74+
context.parser.onImageTap?.call(Uri.decodeFull(data), context,
75+
attributes, context.tree.element);
6876
},
6977
);
7078
});
@@ -73,55 +81,62 @@ CustomRender svgDataImageRender() =>
7381
/// The CustomRender function that renders an <img> tag with a network svg image.
7482
CustomRender svgNetworkImageRender() =>
7583
CustomRender.widget(widget: (context, buildChildren) {
76-
if (context.tree.element?.attributes["src"] == null) {
84+
final attributes =
85+
context.tree.element?.attributes.cast<String, String>() ??
86+
<String, String>{};
87+
88+
if (attributes["src"] == null) {
7789
return const SizedBox(height: 0, width: 0);
7890
}
7991
return Builder(
8092
key: context.key,
8193
builder: (buildContext) {
8294
return GestureDetector(
8395
child: SvgPicture.network(
84-
context.tree.element!.attributes["src"]!,
85-
width: _width(context.tree.element!.attributes.cast()),
86-
height: _height(context.tree.element!.attributes.cast()),
96+
attributes["src"]!,
97+
width: _width(attributes),
98+
height: _height(attributes),
8799
),
88100
onTap: () {
89101
if (MultipleTapGestureDetector.of(buildContext) != null) {
90102
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
91103
}
92-
context.parser.onImageTap?.call(
93-
context.tree.element!.attributes["src"]!,
94-
context,
95-
context.tree.element!.attributes.cast(),
96-
context.tree.element);
104+
context.parser.onImageTap?.call(attributes["src"]!, context,
105+
attributes, context.tree.element);
97106
},
98107
);
99108
});
100109
});
101110

102111
/// The CustomRender function that renders an <img> tag with an svg asset in your app
103-
CustomRender svgAssetImageRender() =>
112+
CustomRender svgAssetImageRender({AssetBundle? bundle}) =>
104113
CustomRender.widget(widget: (context, buildChildren) {
105-
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
106-
null) {
114+
final attributes =
115+
context.tree.element?.attributes.cast<String, String>() ??
116+
<String, String>{};
117+
118+
if (_src(attributes) == null) {
107119
return const SizedBox(height: 0, width: 0);
108120
}
121+
109122
final assetPath = _src(context.tree.element!.attributes.cast())!
110123
.replaceFirst('asset:', '');
111124
return Builder(
112125
key: context.key,
113126
builder: (buildContext) {
114127
return GestureDetector(
115-
child: SvgPicture.asset(assetPath),
128+
child: SvgPicture.asset(
129+
assetPath,
130+
bundle: bundle,
131+
width: _width(attributes),
132+
height: _height(attributes),
133+
),
116134
onTap: () {
117135
if (MultipleTapGestureDetector.of(buildContext) != null) {
118136
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
119137
}
120138
context.parser.onImageTap?.call(
121-
assetPath,
122-
context,
123-
context.tree.element!.attributes.cast(),
124-
context.tree.element);
139+
assetPath, context, attributes, context.tree.element);
125140
},
126141
);
127142
});
@@ -136,10 +151,16 @@ CustomRenderMatcher svgTagMatcher() => (context) {
136151
CustomRenderMatcher svgDataUriMatcher(
137152
{String? encoding = 'base64', String? mime = 'image/svg+xml'}) =>
138153
(context) {
139-
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
140-
null) return false;
141-
final dataUri = _dataUriFormat.firstMatch(
142-
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!);
154+
final attributes =
155+
context.tree.element?.attributes.cast<String, String>() ??
156+
<String, String>{};
157+
158+
if (_src(attributes) == null) {
159+
return false;
160+
}
161+
162+
final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
163+
143164
return context.tree.element?.localName == "img" &&
144165
dataUri != null &&
145166
(mime == null || dataUri.namedGroup('mime') == mime) &&
@@ -153,11 +174,17 @@ CustomRenderMatcher svgNetworkSourceMatcher({
153174
String? extension = "svg",
154175
}) =>
155176
(context) {
156-
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
157-
null) return false;
177+
final attributes =
178+
context.tree.element?.attributes.cast<String, String>() ??
179+
<String, String>{};
180+
181+
if (_src(attributes) == null) {
182+
return false;
183+
}
184+
158185
try {
159-
final src = Uri.parse(_src(
160-
context.tree.element?.attributes.cast() ?? <String, String>{})!);
186+
final src = Uri.parse(_src(attributes)!);
187+
161188
return context.tree.element?.localName == "img" &&
162189
schemas.contains(src.scheme) &&
163190
(domains == null || domains.contains(src.host)) &&
@@ -168,14 +195,16 @@ CustomRenderMatcher svgNetworkSourceMatcher({
168195
};
169196

170197
/// A CustomRenderMatcher for an <img> tag with an in-app svg asset
171-
CustomRenderMatcher svgAssetUriMatcher() => (context) =>
172-
context.tree.element?.localName == "img" &&
173-
_src(context.tree.element?.attributes.cast() ?? <String, String>{}) !=
174-
null &&
175-
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!
176-
.startsWith("asset:") &&
177-
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!
178-
.endsWith(".svg");
198+
CustomRenderMatcher svgAssetUriMatcher() => (context) {
199+
final attributes =
200+
context.tree.element?.attributes.cast<String, String>() ??
201+
<String, String>{};
202+
203+
return context.tree.element?.localName == "img" &&
204+
_src(attributes) != null &&
205+
_src(attributes)!.startsWith("asset:") &&
206+
_src(attributes)!.endsWith(".svg");
207+
};
179208

180209
final _dataUriFormat = RegExp(
181210
"^(?<scheme>data):(?<mime>image\\/[\\w\\+\\-\\.]+)(?<encoding>;base64)?\\,(?<data>.*)");
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:flutter_html_svg/flutter_html_svg.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import './test_utils.dart';
4+
5+
void main() {
6+
group("custom image asset tests:", () {
7+
const String svgString = svgRawString;
8+
String makeImgTag({
9+
String? src,
10+
int? width,
11+
int? height,
12+
}) {
13+
String srcAttr = src != null ? 'src="$src"' : '';
14+
String widthAttr = width != null ? 'width=$width' : '';
15+
String heightAttr = height != null ? 'height=$height' : '';
16+
17+
return """
18+
<img $widthAttr $heightAttr $srcAttr />
19+
""";
20+
}
21+
22+
// Happy path (taken from SvgPicture examples)
23+
testMatchAndRender(
24+
"matches and renders img with asset",
25+
makeImgTag(src: "asset:fake.svg", width: 100, height: 100),
26+
svgAssetUriMatcher(),
27+
svgAssetImageRender(bundle: FakeAssetBundle()),
28+
TestResult.matchAndRenderSvgPicture);
29+
30+
// Failure paths
31+
testMatchAndRender(
32+
"does not match",
33+
makeImgTag(src: "fake.svg"),
34+
svgAssetUriMatcher(),
35+
svgAssetImageRender(bundle: FakeAssetBundle()),
36+
TestResult.noMatch);
37+
});
38+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'package:flutter_html_svg/flutter_html_svg.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import './test_utils.dart';
4+
5+
void main() {
6+
group("custom image data uri tests:", () {
7+
String makeImgTag({
8+
String? src,
9+
int? width,
10+
int? height,
11+
}) {
12+
String srcAttr = src != null ? 'src="$src"' : '';
13+
String widthAttr = width != null ? 'width=$width' : '';
14+
String heightAttr = height != null ? 'height=$height' : '';
15+
16+
return """
17+
<img alt='dummy' $widthAttr $heightAttr $srcAttr />
18+
""";
19+
}
20+
21+
// Happy path (taken from SvgPicture examples)
22+
testMatchAndRender(
23+
"matches and renders image/svg+xml with text encoding",
24+
makeImgTag(
25+
src: 'data:image/svg+xml,$svgEncoded', width: 100, height: 100),
26+
svgDataUriMatcher(encoding: null),
27+
svgDataImageRender(),
28+
TestResult.matchAndRenderSvgPicture);
29+
testMatchAndRender(
30+
"matches and renders image/svg+xml and base64 encoding",
31+
makeImgTag(src: 'data:image/svg+xml;base64,$svgBase64'),
32+
svgDataUriMatcher(),
33+
svgDataImageRender(),
34+
TestResult.matchAndRenderSvgPicture);
35+
36+
// Failure paths
37+
testMatchAndRender("image tag with no attributes", makeImgTag(),
38+
svgDataUriMatcher(), svgDataImageRender(), TestResult.noMatch);
39+
testMatchAndRender(
40+
"does not match base64 image data uri",
41+
makeImgTag(
42+
src:
43+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='),
44+
svgDataUriMatcher(),
45+
svgDataImageRender(),
46+
TestResult.noMatch);
47+
testMatchAndRender(
48+
"does not match non-svg mime data",
49+
makeImgTag(src: 'data:text/plain;base64,'),
50+
svgDataUriMatcher(),
51+
svgDataImageRender(),
52+
TestResult.noMatch);
53+
testMatchAndRender(
54+
"does not match non-data schema",
55+
makeImgTag(src: 'http:'),
56+
svgDataUriMatcher(),
57+
svgDataImageRender(),
58+
TestResult.noMatch);
59+
});
60+
}

0 commit comments

Comments
 (0)