import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:marianum_mobile/widget/prosemirror/pm_document_view.dart'; import 'package:marianum_mobile/widget/prosemirror/pm_node.dart'; const _pngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII='; Map _kitchenSinkDoc() => { 'type': 'doc', 'content': [ { 'type': 'heading', 'attrs': {'level': 1}, 'content': [ {'type': 'text', 'text': 'Heading'}, ], }, { 'type': 'paragraph', 'content': [ { 'type': 'text', 'text': 'bold ', 'marks': [ {'type': 'bold'}, ], }, { 'type': 'text', 'text': 'link', 'marks': [ { 'type': 'link', 'attrs': {'href': 'https://example.org'}, }, ], }, {'type': 'hardBreak'}, { 'type': 'text', 'text': 'code', 'marks': [ {'type': 'code'}, ], }, ], }, { 'type': 'bulletList', 'content': [ { 'type': 'listItem', 'content': [ { 'type': 'paragraph', 'content': [ {'type': 'text', 'text': 'item'}, ], }, ], }, ], }, { 'type': 'callout', 'attrs': {'variant': 'info'}, 'content': [ { 'type': 'paragraph', 'content': [ {'type': 'text', 'text': 'note'}, ], }, ], }, { 'type': 'codeBlock', 'content': [ {'type': 'text', 'text': 'x = 1'}, ], }, {'type': 'horizontalRule'}, { 'type': 'image', 'attrs': {'src': _pngDataUri, 'align': 'center', 'width': '40%'}, }, { 'type': 'unknownWidget', 'content': [ { 'type': 'paragraph', 'content': [ {'type': 'text', 'text': 'fallback child'}, ], }, ], }, { 'type': 'table', 'content': [ { 'type': 'tableRow', 'content': [ { 'type': 'tableHeader', 'attrs': {'colspan': 2}, 'content': [ { 'type': 'paragraph', 'content': [ {'type': 'text', 'text': 'head'}, ], }, ], }, ], }, { 'type': 'tableRow', 'content': [ { 'type': 'tableCell', 'content': [ { 'type': 'paragraph', 'content': [ {'type': 'text', 'text': 'a'}, ], }, ], }, { 'type': 'tableCell', 'content': [ { 'type': 'paragraph', 'content': [ {'type': 'text', 'text': 'b'}, ], }, ], }, ], }, ], }, ], }; Widget _host(Brightness brightness, {void Function(String href)? onLinkTap}) { return MaterialApp( theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF993333), brightness: brightness, ), ), home: Scaffold( body: SingleChildScrollView( child: PmDocumentView( doc: PmNode.fromJson(_kitchenSinkDoc()), onLinkTap: onLinkTap, ), ), ), ); } void main() { testWidgets('renders kitchen-sink doc in light theme without exception', ( tester, ) async { await tester.pumpWidget(_host(Brightness.light)); await tester.pump(); expect(tester.takeException(), isNull); expect(find.text('Heading'), findsOneWidget); expect(find.text('note'), findsOneWidget); expect(find.text('fallback child'), findsOneWidget); }); testWidgets('renders kitchen-sink doc in dark theme without exception', ( tester, ) async { await tester.pumpWidget(_host(Brightness.dark)); await tester.pump(); expect(tester.takeException(), isNull); expect(find.text('Heading'), findsOneWidget); }); testWidgets('link marks route through onLinkTap', (tester) async { final tapped = []; await tester.pumpWidget(_host(Brightness.light, onLinkTap: tapped.add)); await tester.pump(); final richTexts = tester.widgetList(find.byType(RichText)); for (final richText in richTexts) { if (TapGestureRecognizerHarness.tapFirstLink(richText.text)) break; } expect(tapped, ['https://example.org']); }); } /// Walks a composed span tree and fires the first link recognizer it finds, /// returning whether one was tapped. class TapGestureRecognizerHarness { static bool tapFirstLink(InlineSpan span) { var tapped = false; span.visitChildren((child) { if (child is TextSpan) { final recognizer = child.recognizer; if (recognizer is TapGestureRecognizer && recognizer.onTap != null) { recognizer.onTap!(); tapped = true; return false; } } return true; }); return tapped; } }