import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_ui/src/components/html_text.dart'; import 'test_utils.dart'; /// Text.rich creates a nested structure: root -> wrapper -> actual children List _getContentSpans(WidgetTester tester) { final richText = tester.widget(find.byType(RichText)); final root = richText.text as TextSpan; if (root.children?.isNotEmpty ?? false) { final wrapper = root.children!.first; if (wrapper is TextSpan && wrapper.children != null) { return wrapper.children!; } } return []; } TextSpan _findSpan(List spans, String text) { return spans.firstWhere( (span) => span is TextSpan && span.text == text, orElse: () => throw StateError('No span found with text: "$text"'), ) as TextSpan; } String _concatenateText(List spans) { return spans.whereType().map((s) => s.text ?? '').join(); } void _triggerTap(TextSpan span) { final recognizer = span.recognizer; if (recognizer is TapGestureRecognizer) { recognizer.onTap?.call(); } } void main() { group('ImmichHtmlText', () { testWidgets('renders plain text without HTML tags', (tester) async { await tester.pumpTestWidget( const ImmichHtmlText('This is plain text'), ); expect(find.text('This is plain text'), findsOneWidget); }); testWidgets('handles mixed content with bold and links', (tester) async { await tester.pumpTestWidget( ImmichHtmlText( 'This is an example of HTML text with bold.', linkHandlers: {'link': () {}}, ), ); final spans = _getContentSpans(tester); final exampleSpan = _findSpan(spans, 'example'); expect(exampleSpan.style?.fontWeight, FontWeight.bold); final boldSpan = _findSpan(spans, 'bold'); expect(boldSpan.style?.fontWeight, FontWeight.bold); final linkSpan = _findSpan(spans, 'HTML text'); expect(linkSpan.style?.decoration, TextDecoration.underline); expect(linkSpan.style?.fontWeight, FontWeight.bold); expect(linkSpan.recognizer, isA()); expect(_concatenateText(spans), 'This is an example of HTML text with bold.'); }); testWidgets('applies text style properties', (tester) async { await tester.pumpTestWidget( const ImmichHtmlText( 'Test text', style: TextStyle( fontSize: 16, color: Colors.purple, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ); final text = tester.widget(find.byType(Text)); final richText = text.textSpan as TextSpan; expect(richText.style?.fontSize, 16); expect(richText.style?.color, Colors.purple); expect(text.textAlign, TextAlign.center); expect(text.maxLines, 2); expect(text.overflow, TextOverflow.ellipsis); }); testWidgets('handles text with special characters', (tester) async { await tester.pumpTestWidget( const ImmichHtmlText('Text with & < > " \' characters'), ); expect(find.byType(RichText), findsOneWidget); final spans = _getContentSpans(tester); expect(_concatenateText(spans), 'Text with & < > " \' characters'); }); group('bold', () { testWidgets('renders bold text with tag', (tester) async { await tester.pumpTestWidget( const ImmichHtmlText('This is bold text'), ); final spans = _getContentSpans(tester); final boldSpan = _findSpan(spans, 'bold'); expect(boldSpan.style?.fontWeight, FontWeight.bold); expect(_concatenateText(spans), 'This is bold text'); }); testWidgets('renders bold text with tag', (tester) async { await tester.pumpTestWidget( const ImmichHtmlText('This is strong text'), ); final spans = _getContentSpans(tester); final strongSpan = _findSpan(spans, 'strong'); expect(strongSpan.style?.fontWeight, FontWeight.bold); }); testWidgets('handles nested bold tags', (tester) async { await tester.pumpTestWidget( const ImmichHtmlText('Text with bold and nested'), ); final spans = _getContentSpans(tester); final nestedSpan = _findSpan(spans, 'nested'); expect(nestedSpan.style?.fontWeight, FontWeight.bold); final boldSpan = _findSpan(spans, 'bold and '); expect(boldSpan.style?.fontWeight, FontWeight.bold); expect(_concatenateText(spans), 'Text with bold and nested'); }); }); group('link', () { testWidgets('renders link text with tag', (tester) async { await tester.pumpTestWidget( ImmichHtmlText( 'This is a custom link text', linkHandlers: {'link': () {}}, ), ); final spans = _getContentSpans(tester); final linkSpan = _findSpan(spans, 'custom link'); expect(linkSpan.style?.decoration, TextDecoration.underline); expect(linkSpan.recognizer, isA()); }); testWidgets('handles link tap with callback', (tester) async { var linkTapped = false; await tester.pumpTestWidget( ImmichHtmlText( 'Tap here', linkHandlers: {'link': () => linkTapped = true}, ), ); final spans = _getContentSpans(tester); final linkSpan = _findSpan(spans, 'here'); expect(linkSpan.recognizer, isA()); _triggerTap(linkSpan); expect(linkTapped, isTrue); }); testWidgets('handles custom prefixed link tags', (tester) async { await tester.pumpTestWidget( ImmichHtmlText( 'Refer to docs and other', linkHandlers: { 'docs-link': () {}, 'other-link': () {}, }, ), ); final spans = _getContentSpans(tester); final docsSpan = _findSpan(spans, 'docs'); final otherSpan = _findSpan(spans, 'other'); expect(docsSpan.style?.decoration, TextDecoration.underline); expect(otherSpan.style?.decoration, TextDecoration.underline); }); testWidgets('applies custom link style', (tester) async { const customLinkStyle = TextStyle( color: Colors.red, decoration: TextDecoration.overline, ); await tester.pumpTestWidget( ImmichHtmlText( 'Click here', linkStyle: customLinkStyle, linkHandlers: {'link': () {}}, ), ); final spans = _getContentSpans(tester); final linkSpan = _findSpan(spans, 'here'); expect(linkSpan.style?.color, Colors.red); expect(linkSpan.style?.decoration, TextDecoration.overline); }); testWidgets('link without handler renders but is not tappable', (tester) async { await tester.pumpTestWidget( ImmichHtmlText( 'Link without handler: click me', linkHandlers: {'other-link': () {}}, ), ); final spans = _getContentSpans(tester); final linkSpan = _findSpan(spans, 'click me'); expect(linkSpan.style?.decoration, TextDecoration.underline); expect(linkSpan.recognizer, isNull); }); testWidgets('handles multiple links with different handlers', (tester) async { var firstLinkTapped = false; var secondLinkTapped = false; await tester.pumpTestWidget( ImmichHtmlText( 'Go to docs or help', linkHandlers: { 'docs-link': () => firstLinkTapped = true, 'help-link': () => secondLinkTapped = true, }, ), ); final spans = _getContentSpans(tester); final docsSpan = _findSpan(spans, 'docs'); final helpSpan = _findSpan(spans, 'help'); _triggerTap(docsSpan); expect(firstLinkTapped, isTrue); expect(secondLinkTapped, isFalse); _triggerTap(helpSpan); expect(secondLinkTapped, isTrue); }); }); }); }