TF-4054 Fix link detection
This commit is contained in:
@@ -322,4 +322,381 @@ void main() {
|
||||
expect(HtmlUtils.extractPlainText(html), '<heloloasdadadadadsad>dab');
|
||||
});
|
||||
});
|
||||
|
||||
group('HtmlUtils.wrapPlainTextLinks', () {
|
||||
test('wraps plain text link inside <p>', () {
|
||||
const input = '<div><p>https://google.com</p></div>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<div><p><a href="https://google.com">https://google.com</a></p></div>',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps existing <a> tags unchanged', () {
|
||||
const input = "<div><a href='https://google.com'>Hello</a></div>";
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
"<div><a href=\"https://google.com\">Hello</a></div>",
|
||||
);
|
||||
});
|
||||
|
||||
test('wraps multiple links in text', () {
|
||||
const input = '<p>Go to https://flutter.dev and https://dart.dev now</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Go to <a href="https://flutter.dev">https://flutter.dev</a> and <a href="https://dart.dev">https://dart.dev</a> now</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps non-link text unchanged', () {
|
||||
const input = '<p>Hello world!</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(result.trim(), '<p>Hello world!</p>');
|
||||
});
|
||||
|
||||
test('handles mixed content with existing and new links', () {
|
||||
const input = '''
|
||||
<div>
|
||||
<p>Visit https://google.com or <a href="https://flutter.dev">Flutter</a></p>
|
||||
</div>
|
||||
''';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<div>\n<p>Visit <a href="https://google.com">https://google.com</a> or <a href="https://flutter.dev">Flutter</a></p>\n</div>',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns input if body is null', () {
|
||||
const input = '';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(result, input);
|
||||
});
|
||||
|
||||
test('handles link with trailing punctuation', () {
|
||||
const input = '<p>Check https://google.com, it is great!</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Check <a href="https://google.com">https://google.com</a>, it is great!</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link with query parameters', () {
|
||||
const input = '<p>Go to https://example.com/search?q=test</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Go to <a href="https://example.com/search?q=test">https://example.com/search?q=test</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('wraps link inside span', () {
|
||||
const input = '<span>See https://flutter.dev/docs for details</span>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<span>See <a href="https://flutter.dev/docs">https://flutter.dev/docs</a> for details</span>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles multiple paragraphs with links', () {
|
||||
const input = '''
|
||||
<div>
|
||||
<p>Visit https://google.com</p>
|
||||
<p>Then go to https://flutter.dev</p>
|
||||
</div>
|
||||
''';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<div>\n <p>Visit <a href="https://google.com">https://google.com</a></p>\n <p>Then go to <a href="https://flutter.dev">https://flutter.dev</a></p>\n</div>',
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores links already wrapped in <a>', () {
|
||||
const input =
|
||||
'<p><a href="https://google.com">https://google.com</a> is wrapped</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p><a href="https://google.com">https://google.com</a> is wrapped</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link with trailing parenthesis correctly', () {
|
||||
const input = '<p>(https://example.com)</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>(<a href="https://example.com">https://example.com</a>)</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link followed by exclamation mark', () {
|
||||
const input = '<p>Wow https://amazing.dev!</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Wow <a href="https://amazing.dev">https://amazing.dev</a>!</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link followed by question mark', () {
|
||||
const input = '<p>What is https://example.com?</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>What is <a href="https://example.com">https://example.com</a>?</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles http (non-https) link', () {
|
||||
const input = '<p>Visit http://example.org for info</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Visit <a href="http://example.org">http://example.org</a> for info</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles multiple text segments and links mixed', () {
|
||||
const input =
|
||||
'<p>Before https://link1.com middle https://link2.com end</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Before <a href="https://link1.com">https://link1.com</a> middle <a href="https://link2.com">https://link2.com</a> end</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles sentence with link between words', () {
|
||||
const input = '<p>Check this https://docs.flutter.dev today</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Check this <a href="https://docs.flutter.dev">https://docs.flutter.dev</a> today</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link at beginning of text', () {
|
||||
const input = '<p>https://start.com is the first link</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p><a href="https://start.com">https://start.com</a> is the first link</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link at end of text', () {
|
||||
const input = '<p>Go to end https://end.com</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Go to end <a href="https://end.com">https://end.com</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles text containing only the link', () {
|
||||
const input = '<p>https://onlylink.com</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p><a href="https://onlylink.com">https://onlylink.com</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('wraps link starting with www', () {
|
||||
const input = '<p>Visit www.google.com for info</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Visit <a href="https://www.google.com">www.google.com</a> for info</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link followed by colon', () {
|
||||
const input = '<p>URL: https://flutter.dev:</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>URL: <a href="https://flutter.dev">https://flutter.dev</a>:</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles parentheses around link', () {
|
||||
const input = '<p>(https://parenthesis.com)</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>(<a href="https://parenthesis.com">https://parenthesis.com</a>)</p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link inside nested span', () {
|
||||
const input = '<p><span>Check https://nested.com inside span</span></p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p><span>Check <a href="https://nested.com">https://nested.com</a> inside span</span></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles multiple links across multiple tags', () {
|
||||
const input = '<p>First https://one.com</p><p>Second https://two.com</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>First <a href="https://one.com">https://one.com</a></p><p>Second <a href="https://two.com">https://two.com</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link with port number', () {
|
||||
const input = '<p>Visit http://localhost:8080/home</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Visit <a href="http://localhost:8080/home">http://localhost:8080/home</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles unicode domain', () {
|
||||
const input = '<p>Try https://例子.测试</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Try <a href="https://例子.测试">https://例子.测试</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles link with query params & escapes correctly', () {
|
||||
const input = '<p>Search https://google.com?q=flutter&lang=en</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<p>Search <a href="https://google.com?q=flutter&lang=en">https://google.com?q=flutter&lang=en</a></p>',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not wrap text inside existing <a>', () {
|
||||
const input = '<a href="https://flutter.dev">https://flutter.dev</a>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(
|
||||
result.trim(),
|
||||
'<a href="https://flutter.dev">https://flutter.dev</a>',
|
||||
);
|
||||
});
|
||||
|
||||
test('handles empty html safely', () {
|
||||
const input = '';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(result, '');
|
||||
});
|
||||
|
||||
test('handles text without link', () {
|
||||
const input = '<p>No link here</p>';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(result.trim(), '<p>No link here</p>');
|
||||
});
|
||||
|
||||
test('handles invalid HTML gracefully', () {
|
||||
const input = '<p>Some broken <div> www.test.com';
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
expect(result.contains('<a href="https://www.test.com">'), true);
|
||||
});
|
||||
|
||||
test('ignores URLs inside img/video/link/script attributes', () {
|
||||
const input = '''
|
||||
<div>
|
||||
<p>Visit www.google.com</p>
|
||||
<img src="https://example.com/image.jpg" alt="example">
|
||||
<video src="https://example.com/video.mp4"></video>
|
||||
<a href="https://flutter.dev">Flutter</a>
|
||||
</div>
|
||||
''';
|
||||
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
|
||||
expect(
|
||||
result.contains('<a href="https://www.google.com">www.google.com</a>'),
|
||||
true,
|
||||
reason: 'Text link www.google.com should be wrapped',
|
||||
);
|
||||
|
||||
expect(result.contains('<img src="https://example.com/image.jpg"'), true);
|
||||
expect(result.contains('<video src="https://example.com/video.mp4"'), true);
|
||||
expect(result.contains('<a href="https://flutter.dev">Flutter</a>'), true);
|
||||
});
|
||||
|
||||
test('does not alter src or href attributes', () {
|
||||
const input = '''
|
||||
<div>
|
||||
<p>Visit www.google.com</p>
|
||||
<img src="https://example.com/image.jpg" alt="example">
|
||||
<video src="https://example.com/video.mp4"></video>
|
||||
<a href="https://flutter.dev">Flutter</a>
|
||||
</div>
|
||||
''';
|
||||
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input).trim();
|
||||
|
||||
expect(
|
||||
result,
|
||||
'<div>\n <p>Visit <a href="https://www.google.com">www.google.com</a></p>\n '
|
||||
'<img src="https://example.com/image.jpg" alt="example">\n '
|
||||
'<video src="https://example.com/video.mp4"></video>\n '
|
||||
'<a href="https://flutter.dev">Flutter</a>\n</div>',
|
||||
);
|
||||
});
|
||||
|
||||
test('does NOT wrap URLs inside special tags (img, video, script, link, iframe)', () {
|
||||
const input = '''
|
||||
<div>
|
||||
<p>Check www.google.com</p>
|
||||
<img src="https://cdn.example.com/image.jpg" alt="Sample image with link google.com">
|
||||
<video src="https://cdn.example.com/video.mp4">
|
||||
www.google.com
|
||||
</video>
|
||||
<script>console.log("Visit www.google.com");</script>
|
||||
<iframe src="https://player.example.com"></iframe>
|
||||
<link rel="stylesheet" href="https://cdn.example.com/style.css">
|
||||
</div>
|
||||
''';
|
||||
|
||||
final result = HtmlUtils.wrapPlainTextLinks(input);
|
||||
// The URL in <p> should be wrapped
|
||||
expect(
|
||||
result.contains('<a href="https://www.google.com">www.google.com</a>'),
|
||||
true,
|
||||
reason:
|
||||
'The text www.google.com inside <p> should be wrapped into a link',
|
||||
);
|
||||
|
||||
// There must NOT be any <a> tag inside these special tags
|
||||
final forbiddenTags = ['img', 'video', 'script', 'iframe', 'link'];
|
||||
for (final tag in forbiddenTags) {
|
||||
final pattern = RegExp('<$tag[^>]*>.*?<\\/\\s*$tag>', dotAll: true);
|
||||
final matches = pattern.allMatches(result);
|
||||
for (final match in matches) {
|
||||
final segment = match.group(0)!;
|
||||
expect(
|
||||
segment.contains('<a '),
|
||||
false,
|
||||
reason: 'There must be no <a> tag inside <$tag>',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// There must NOT be any <a> tag inside href/src attributes
|
||||
final attributePattern = RegExp(r'(src|href)="[^"]*<a[^"]*"');
|
||||
expect(
|
||||
attributePattern.hasMatch(result),
|
||||
false,
|
||||
reason: 'There must be no <a> tag inside href or src attributes',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user