import 'package:eatvio_app/common/widgets/markdown_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('MarkdownEditor', () { testWidgets('отображается как виджет', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor(), ), ), ); expect(find.byType(MarkdownEditor), findsOneWidget); }); testWidgets('инициализируется с initialText', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor(initialText: 'Initial text'), ), ), ); expect(find.byType(MarkdownEditor), findsOneWidget); }); testWidgets('отображает hintText', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor(hintText: 'Enter text here'), ), ), ); expect(find.byType(MarkdownEditor), findsOneWidget); }); testWidgets('вызывает onChanged при изменении текста', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, ), ), ), ); final textField = find.byType(TextField); expect(textField, findsOneWidget); await tester.enterText(textField, 'Test'); await tester.pump(); // onChanged должен быть вызван expect(changedValue, isNotNull); }); testWidgets('отображает toolbar когда showInlineToolbar = true', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor(showInlineToolbar: true), ), ), ); await tester.pump(); // Toolbar должен быть виден expect(find.byIcon(Icons.format_bold), findsOneWidget); }); testWidgets('скрывает toolbar когда showInlineToolbar = false', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor(showInlineToolbar: false), ), ), ); await tester.pump(); // Toolbar не должен быть виден expect(find.byIcon(Icons.format_bold), findsNothing); }); testWidgets('применяет форматирование жирным', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); // Нажимаем кнопку жирного await tester.tap(find.byIcon(Icons.format_bold)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('применяет форматирование курсивом', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); await tester.tap(find.byIcon(Icons.format_italic)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('применяет заголовок', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); // Находим кнопку заголовка (текст 'H') final headingButton = find.text('H'); await tester.tap(headingButton); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('применяет нумерованный список', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); await tester.tap(find.byIcon(Icons.format_list_numbered)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('применяет маркированный список', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); await tester.tap(find.byIcon(Icons.format_list_bulleted)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('применяет ссылку', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); await tester.tap(find.byIcon(Icons.link)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('применяет горизонтальную линию', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.enterText(textField, 'Test'); await tester.pump(); await tester.tap(textField); await tester.pump(); await tester.tap(find.byIcon(Icons.horizontal_rule)); await tester.pump(); expect(changedValue, contains('---')); }); testWidgets('переключает режим preview', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '# Test', showInlineToolbar: true, ), ), ), ); await tester.pump(); // Изначально должен быть режим редактирования expect(find.byType(TextField), findsOneWidget); // Нажимаем кнопку preview await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Должен быть режим preview expect(find.byType(TextField), findsNothing); // Нажимаем кнопку редактирования await tester.tap(find.byIcon(Icons.edit)); await tester.pumpAndSettle(); // Должен вернуться режим редактирования expect(find.byType(TextField), findsOneWidget); }); testWidgets('вызывает onToolbarVisibilityChanged при изменении видимости', (WidgetTester tester) async { bool? toolbarVisible; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( showInlineToolbar: true, onToolbarVisibilityChanged: (visible) => toolbarVisible = visible, ), ), ), ); await tester.pumpAndSettle(); expect(toolbarVisible, isNotNull); }); testWidgets('удаляет форматирование жирным если уже применено', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '**Test**', onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.tap(textField); await tester.pump(); await tester.tap(find.byIcon(Icons.format_bold)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('обрабатывает ссылку в формате markdown', (WidgetTester tester) async { String? changedValue; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '[Link](url)', onChanged: (value) => changedValue = value, showInlineToolbar: true, ), ), ), ); await tester.pump(); final textField = find.byType(TextField); await tester.tap(textField); await tester.pump(); await tester.tap(find.byIcon(Icons.link)); await tester.pump(); expect(changedValue, isNotNull); }); testWidgets('отображает preview с заголовком', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '# Heading', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Заголовок рендерится через Text виджет expect(find.textContaining('Heading'), findsWidgets); }); testWidgets('отображает preview с маркированным списком', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '- Item 1\n- Item 2', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Проверяем, что preview отображается (RichText виджеты должны быть) expect(find.byType(RichText), findsWidgets); // Проверяем наличие текста через RichText final richTexts = find.byType(RichText); expect(richTexts, findsWidgets); }); testWidgets('отображает preview с нумерованным списком', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '1. First\n2. Second', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Проверяем, что preview отображается (RichText виджеты должны быть) expect(find.byType(RichText), findsWidgets); }); testWidgets('отображает preview с горизонтальной линией', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '---', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.byType(Divider), findsOneWidget); }); testWidgets('обрабатывает hasError параметр', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( hasError: true, ), ), ), ); expect(find.byType(MarkdownEditor), findsOneWidget); }); testWidgets('обрабатывает minLines и maxLines', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( minLines: 2, maxLines: 5, ), ), ), ); expect(find.byType(MarkdownEditor), findsOneWidget); }); testWidgets('обрабатывает heightPreview', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( heightPreview: 200, initialText: 'Test', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.byType(MarkdownEditor), findsOneWidget); }); testWidgets('обрабатывает пустой текст в preview', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( hintText: 'Enter text', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.text('Enter text'), findsOneWidget); }); testWidgets('обрабатывает многострочный текст с форматированием', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '# Title\n\n**Bold** text\n\n- List item', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Проверяем, что preview отображается (должны быть Text и RichText виджеты) expect(find.byType(Text), findsWidgets); expect(find.byType(RichText), findsWidgets); }); testWidgets('обрабатывает встроенное форматирование в тексте', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'This is **bold** and this is *italic*', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Проверяем, что preview отображается (RichText должен быть) expect(find.byType(RichText), findsWidgets); }); testWidgets('обрабатывает ссылки в тексте', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'Check [this link](https://example.com)', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Проверяем, что preview отображается (RichText должен быть) expect(find.byType(RichText), findsWidgets); }); testWidgets('обрабатывает заголовки разных уровней', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '# H1\n## H2\n### H3', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.text('H1'), findsOneWidget); expect(find.text('H2'), findsOneWidget); expect(find.text('H3'), findsOneWidget); }); testWidgets('обрабатывает пустые строки в preview', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'Line 1\n\nLine 2', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Проверяем, что preview отображается (RichText должен быть) expect(find.byType(RichText), findsWidgets); }); testWidgets('preview: маркер списка со звёздочкой', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '* Asterisk item', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); // Тело пункта в RichText/TextSpan — find.textContaining не всегда его видит expect(find.byType(RichText), findsWidgets); expect(find.textContaining('•'), findsWidgets); }); testWidgets('preview: нумерация из двух цифр', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '10. Tenth row', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.byType(RichText), findsWidgets); expect(find.textContaining('10.'), findsWidgets); }); testWidgets(' после выхода из preview выполняется отложенный requestFocus', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'focus', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.edit)); await tester.pump(); await tester.pump(const Duration(milliseconds: 150)); expect(find.byType(TextField), findsOneWidget); }); testWidgets(' снимает разметку ссылки когда выделен markdown-link', (WidgetTester tester) async { String? changed; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '[Label](https://example.com)', showInlineToolbar: true, onChanged: (v) => changed = v, ), ), ), ); await tester.pump(); final field = tester.widget(find.byType(TextField)); final controller = field.controller!; controller.selection = TextSelection(baseOffset: 0, extentOffset: controller.text.length); await tester.tap(find.byIcon(Icons.link)); await tester.pump(); expect(changed, contains('Label')); expect(changed, isNot(contains('[Label](https://example.com)'))); }); testWidgets('preview: H2 heading renders larger heading style', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '## Subtitle', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.text('Subtitle'), findsOneWidget); }); testWidgets('preview: H3 heading renders heading text', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: '### Detail', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.text('Detail'), findsOneWidget); }); testWidgets('preview: horizontal rule renders Divider', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'Above\n---\nBelow', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.byType(Divider), findsOneWidget); }); testWidgets('preview: italic span renders inside RichText', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'A *cursive* word', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.byType(RichText), findsWidgets); }); testWidgets('preview: link span renders inside RichText', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'Visit [the docs](https://example.com)', showInlineToolbar: true, ), ), ), ); await tester.pump(); await tester.tap(find.byIcon(Icons.visibility)); await tester.pumpAndSettle(); expect(find.byType(RichText), findsWidgets); }); testWidgets('toolbar buttons mutate text via onChanged when cursor is set', (WidgetTester tester) async { String? changed; await tester.pumpWidget( MaterialApp( home: Scaffold( body: MarkdownEditor( initialText: 'plain', showInlineToolbar: true, onChanged: (v) => changed = v, ), ), ), ); await tester.pump(); final field = tester.widget(find.byType(TextField)); final controller = field.controller!; controller.selection = const TextSelection.collapsed(offset: 0); await tester.tap(find.byIcon(Icons.format_bold)); await tester.pump(); expect(changed, contains('**')); controller.text = 'plain'; controller.selection = const TextSelection.collapsed(offset: 0); await tester.tap(find.byIcon(Icons.format_italic)); await tester.pump(); expect(changed, contains('*')); controller.text = 'plain'; controller.selection = const TextSelection.collapsed(offset: 0); await tester.tap(find.byIcon(Icons.horizontal_rule)); await tester.pump(); expect(changed, contains('---')); controller.text = 'plain'; controller.selection = const TextSelection.collapsed(offset: 0); await tester.tap(find.byIcon(Icons.link)); await tester.pump(); expect(changed, contains('](url)')); }); }); }