TF-4054 Fix link detection

This commit is contained in:
dab246
2025-10-01 00:17:24 +07:00
committed by Dat H. Pham
parent 05b11fb779
commit aa710113b0
3 changed files with 453 additions and 0 deletions
+377
View File
@@ -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&amp;lang=en">https://google.com?q=flutter&amp;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',
);
});
});
}