Skip to content

Commit

Permalink
Adding tests for SVG and fixing height/width bug
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
jlyonsmith committed Dec 7, 2022
1 parent c75e0df commit d13eb9e
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 164 deletions.
2 changes: 2 additions & 0 deletions packages/flutter_html_svg/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
.pub-cache/
.pub/
build/
coverage/
scratch/

# Android related
**/android/**/gradle-wrapper.jar
Expand Down
133 changes: 81 additions & 52 deletions packages/flutter_html_svg/lib/flutter_html_svg.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
library flutter_html_svg;

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
// ignore: implementation_imports
Expand All @@ -11,16 +10,18 @@ import 'package:flutter_svg/flutter_svg.dart';
/// The CustomRender function that renders the <svg> HTML tag.
CustomRender svgTagRender() =>
CustomRender.widget(widget: (context, buildChildren) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

return Builder(
key: context.key,
builder: (buildContext) {
return GestureDetector(
child: SvgPicture.string(
context.tree.element?.outerHtml ?? "",
width: double.tryParse(
context.tree.element?.attributes['width'] ?? ""),
height: double.tryParse(
context.tree.element?.attributes['width'] ?? ""),
width: _width(attributes),
height: _height(attributes),
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
Expand All @@ -29,7 +30,7 @@ CustomRender svgTagRender() =>
context.parser.onImageTap?.call(
context.tree.element?.outerHtml ?? "",
context,
context.tree.element!.attributes.cast(),
attributes,
context.tree.element);
},
);
Expand All @@ -39,32 +40,39 @@ CustomRender svgTagRender() =>
/// The CustomRender function that renders an <img> tag with hardcoded svg data.
CustomRender svgDataImageRender() =>
CustomRender.widget(widget: (context, buildChildren) {
final dataUri = _dataUriFormat.firstMatch(
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!);
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};
final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
final data = dataUri?.namedGroup('data');
if (data == null) return const SizedBox(height: 0, width: 0);

if (data == null || data.isEmpty) {
return const SizedBox(height: 0, width: 0);
}
return Builder(
key: context.key,
builder: (buildContext) {
final width = _width(attributes);
final height = _height(attributes);

return GestureDetector(
child: dataUri?.namedGroup('encoding') == ';base64'
? SvgPicture.memory(
base64.decode(data.trim()),
width: _width(context.tree.element?.attributes.cast() ??
<String, String>{}),
height: _height(context.tree.element?.attributes.cast() ??
<String, String>{}),
width: width,
height: height,
)
: SvgPicture.string(Uri.decodeFull(data)),
: SvgPicture.string(
Uri.decodeFull(data),
width: width,
height: height,
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
context.parser.onImageTap?.call(
Uri.decodeFull(data),
context,
context.tree.element!.attributes.cast(),
context.tree.element);
context.parser.onImageTap?.call(Uri.decodeFull(data), context,
attributes, context.tree.element);
},
);
});
Expand All @@ -73,55 +81,62 @@ CustomRender svgDataImageRender() =>
/// The CustomRender function that renders an <img> tag with a network svg image.
CustomRender svgNetworkImageRender() =>
CustomRender.widget(widget: (context, buildChildren) {
if (context.tree.element?.attributes["src"] == null) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (attributes["src"] == null) {
return const SizedBox(height: 0, width: 0);
}
return Builder(
key: context.key,
builder: (buildContext) {
return GestureDetector(
child: SvgPicture.network(
context.tree.element!.attributes["src"]!,
width: _width(context.tree.element!.attributes.cast()),
height: _height(context.tree.element!.attributes.cast()),
attributes["src"]!,
width: _width(attributes),
height: _height(attributes),
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
context.parser.onImageTap?.call(
context.tree.element!.attributes["src"]!,
context,
context.tree.element!.attributes.cast(),
context.tree.element);
context.parser.onImageTap?.call(attributes["src"]!, context,
attributes, context.tree.element);
},
);
});
});

/// The CustomRender function that renders an <img> tag with an svg asset in your app
CustomRender svgAssetImageRender() =>
CustomRender svgAssetImageRender({AssetBundle? bundle}) =>
CustomRender.widget(widget: (context, buildChildren) {
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
null) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (_src(attributes) == null) {
return const SizedBox(height: 0, width: 0);
}

final assetPath = _src(context.tree.element!.attributes.cast())!
.replaceFirst('asset:', '');
return Builder(
key: context.key,
builder: (buildContext) {
return GestureDetector(
child: SvgPicture.asset(assetPath),
child: SvgPicture.asset(
assetPath,
bundle: bundle,
width: _width(attributes),
height: _height(attributes),
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
context.parser.onImageTap?.call(
assetPath,
context,
context.tree.element!.attributes.cast(),
context.tree.element);
assetPath, context, attributes, context.tree.element);
},
);
});
Expand All @@ -136,10 +151,16 @@ CustomRenderMatcher svgTagMatcher() => (context) {
CustomRenderMatcher svgDataUriMatcher(
{String? encoding = 'base64', String? mime = 'image/svg+xml'}) =>
(context) {
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
null) return false;
final dataUri = _dataUriFormat.firstMatch(
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!);
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (_src(attributes) == null) {
return false;
}

final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);

return context.tree.element?.localName == "img" &&
dataUri != null &&
(mime == null || dataUri.namedGroup('mime') == mime) &&
Expand All @@ -153,11 +174,17 @@ CustomRenderMatcher svgNetworkSourceMatcher({
String? extension = "svg",
}) =>
(context) {
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
null) return false;
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (_src(attributes) == null) {
return false;
}

try {
final src = Uri.parse(_src(
context.tree.element?.attributes.cast() ?? <String, String>{})!);
final src = Uri.parse(_src(attributes)!);

return context.tree.element?.localName == "img" &&
schemas.contains(src.scheme) &&
(domains == null || domains.contains(src.host)) &&
Expand All @@ -168,14 +195,16 @@ CustomRenderMatcher svgNetworkSourceMatcher({
};

/// A CustomRenderMatcher for an <img> tag with an in-app svg asset
CustomRenderMatcher svgAssetUriMatcher() => (context) =>
context.tree.element?.localName == "img" &&
_src(context.tree.element?.attributes.cast() ?? <String, String>{}) !=
null &&
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!
.startsWith("asset:") &&
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!
.endsWith(".svg");
CustomRenderMatcher svgAssetUriMatcher() => (context) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

return context.tree.element?.localName == "img" &&
_src(attributes) != null &&
_src(attributes)!.startsWith("asset:") &&
_src(attributes)!.endsWith(".svg");
};

final _dataUriFormat = RegExp(
"^(?<scheme>data):(?<mime>image\\/[\\w\\+\\-\\.]+)(?<encoding>;base64)?\\,(?<data>.*)");
Expand Down
38 changes: 38 additions & 0 deletions packages/flutter_html_svg/test/svg_asset_image_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter_html_svg/flutter_html_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import './test_utils.dart';

void main() {
group("custom image asset tests:", () {
const String svgString = svgRawString;
String makeImgTag({
String? src,
int? width,
int? height,
}) {
String srcAttr = src != null ? 'src="$src"' : '';
String widthAttr = width != null ? 'width=$width' : '';
String heightAttr = height != null ? 'height=$height' : '';

return """
<img $widthAttr $heightAttr $srcAttr />
""";
}

// Happy path (taken from SvgPicture examples)
testMatchAndRender(
"matches and renders img with asset",
makeImgTag(src: "asset:fake.svg", width: 100, height: 100),
svgAssetUriMatcher(),
svgAssetImageRender(bundle: FakeAssetBundle()),
TestResult.matchAndRenderSvgPicture);

// Failure paths
testMatchAndRender(
"does not match",
makeImgTag(src: "fake.svg"),
svgAssetUriMatcher(),
svgAssetImageRender(bundle: FakeAssetBundle()),
TestResult.noMatch);
});
}
60 changes: 60 additions & 0 deletions packages/flutter_html_svg/test/svg_data_image_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter_html_svg/flutter_html_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import './test_utils.dart';

void main() {
group("custom image data uri tests:", () {
String makeImgTag({
String? src,
int? width,
int? height,
}) {
String srcAttr = src != null ? 'src="$src"' : '';
String widthAttr = width != null ? 'width=$width' : '';
String heightAttr = height != null ? 'height=$height' : '';

return """
<img alt='dummy' $widthAttr $heightAttr $srcAttr />
""";
}

// Happy path (taken from SvgPicture examples)
testMatchAndRender(
"matches and renders image/svg+xml with text encoding",
makeImgTag(
src: 'data:image/svg+xml,$svgEncoded', width: 100, height: 100),
svgDataUriMatcher(encoding: null),
svgDataImageRender(),
TestResult.matchAndRenderSvgPicture);
testMatchAndRender(
"matches and renders image/svg+xml and base64 encoding",
makeImgTag(src: 'data:image/svg+xml;base64,$svgBase64'),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.matchAndRenderSvgPicture);

// Failure paths
testMatchAndRender("image tag with no attributes", makeImgTag(),
svgDataUriMatcher(), svgDataImageRender(), TestResult.noMatch);
testMatchAndRender(
"does not match base64 image data uri",
makeImgTag(
src:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.noMatch);
testMatchAndRender(
"does not match non-svg mime data",
makeImgTag(src: 'data:text/plain;base64,'),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.noMatch);
testMatchAndRender(
"does not match non-data schema",
makeImgTag(src: 'http:'),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.noMatch);
});
}
Loading

0 comments on commit d13eb9e

Please sign in to comment.