From db97ee02cdcba90fc9e2950d4f1c779e8ac27d10 Mon Sep 17 00:00:00 2001 From: pavanpodila Date: Thu, 4 Apr 2024 20:44:14 +0530 Subject: [PATCH] chore: adding more tests, fixing some interfaces --- .../lib/ui/portable_text_block.dart | 68 ++++++----- .../lib/ui/portable_text_config.dart | 39 +++++-- .../test/portable_text_test.dart | 106 +++++++++++++++++- 3 files changed, 166 insertions(+), 47 deletions(-) diff --git a/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart b/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart index d0aab768..c0bfe81b 100644 --- a/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart +++ b/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_block.dart @@ -54,18 +54,20 @@ class PortableTextBlock extends StatelessWidget { ) { final config = PortableTextConfig.shared; + // Step 1: Start with the base style final baseStyle = config.baseStyle(context) ?? theme.textTheme.bodyLarge!; final styleBuilder = config.styles[model.style]; if (styleBuilder == null) { - return WidgetSpan( - child: ErrorView(message: 'Missing style for ${model.style}')); + return _errorSpan('Missing style for ${model.style}'); } var style = config.styles[model.style]?.call(context, baseStyle) ?? baseStyle; final pendingMarkDefs = []; + + // Step 2: Accumulate the styles across all marks for (final mark in span.marks) { /// Standard marks (aka annotations) final builder = config.styles[mark]; @@ -78,67 +80,61 @@ class PortableTextBlock extends StatelessWidget { final markDef = model.markDefs .firstWhereOrNull((final element) => element.key == mark); + // A custom mark exists on this span but no corresponding markDef was found if (markDef == null) { - continue; + return _errorSpan('Missing markDef for "$mark"'); } pendingMarkDefs.add(markDef); } - String? errorText; - - // Build the style across all marks + // Step 3: Continue building styles for custom markDefs for (final markDef in pendingMarkDefs) { final descriptor = config.markDefs[markDef.type]; if (descriptor == null) { - errorText = 'Missing markDef descriptor for "${markDef.type}"'; - continue; + return _errorSpan('Missing markDef descriptor for "${markDef.type}"'); } style = descriptor.styleBuilder?.call(context, markDef, style) ?? style; } - // Now build the final InlineSpan, if any + // Step 4: Now build the final InlineSpan, if any InlineSpan? inlineSpan; int totalSpans = 0; - // Only go ahead if there are no errors from previous step - if (errorText == null) { - for (final markDef in pendingMarkDefs) { - final descriptor = config.markDefs[markDef.type]; - inlineSpan = - descriptor?.spanBuilder?.call(context, markDef, span.text, style); - - if (inlineSpan == null) { - continue; - } - - totalSpans++; - if (totalSpans > 1) { - final markDefChain = pendingMarkDefs.map((e) => e.type).join(' -> '); - errorText = ''' + for (final markDef in pendingMarkDefs) { + final descriptor = config.markDefs[markDef.type]; + inlineSpan = + descriptor?.spanBuilder?.call(context, markDef, span.text, style); + + if (inlineSpan == null) { + continue; + } + + totalSpans++; + if (totalSpans > 1) { + final markDefChain = pendingMarkDefs.map((e) => e.type).join(' -> '); + return _errorSpan(''' We currently support a single custom markDef generating the InlineSpan. We found $totalSpans. The chain of markDefs was: $markDefChain. Suggestion: Try to refactor your custom markDef chain to only generate a single InlineSpan. -You can rely on TextStyles instead for custom styling.'''; - break; - } +You can rely on TextStyles instead for custom styling.'''); } } - if (errorText != null) { - return WidgetSpan( - child: ErrorView( - message: errorText, - asBlock: false, - ), - ); - } - return inlineSpan ?? TextSpan(text: span.text, style: style); } + WidgetSpan _errorSpan(final String message, {final bool asBlock = true}) { + return WidgetSpan( + child: ErrorView( + message: message, + asBlock: asBlock, + ), + ); + } + InlineSpan _bulletMark(final BuildContext context) { final textStyle = PortableTextConfig.shared.baseStyle(context); diff --git a/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_config.dart b/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_config.dart index 32e52086..600c4ec6 100644 --- a/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_config.dart +++ b/packages/sanity/flutter_sanity_portable_text/lib/ui/portable_text_config.dart @@ -24,7 +24,7 @@ typedef BlockWidgetBuilder = Widget Function( /// configuration is based on the Material Design guidelines. /// /// Note that the configuration is shared across all instances of the [PortableText] widget. -class PortableTextConfig { +final class PortableTextConfig { /// The styles used to render the Portable Text content. The keys are the style names used /// in the Portable Text content, such as "h1", "h2", "blockquote", etc. The default styles /// are based on the Material Design typography guidelines. You can customize the styles by @@ -51,14 +51,13 @@ class PortableTextConfig { final Map markDefs = {}; /// The indentation used for list items. The default value is 16. - double listIndent = 16; + double listIndent = defaultListIndent; /// The padding used for list items. The default value is 8. - EdgeInsets itemPadding = const EdgeInsets.only(bottom: 8); + EdgeInsets itemPadding = defaultItemPadding; /// The base style used for rendering the Portable Text content. The default value is the bodyMedium style from the theme. - TextStyle? Function(BuildContext) baseStyle = - (context) => Theme.of(context).textTheme.bodyMedium; + TextStyle? Function(BuildContext) baseStyle = defaultBaseStyle; /// The shared instance of the PortableTextConfig. This instance is used by all [PortableText] widgets /// in the application. You can customize the configuration by calling the [apply] method. @@ -68,12 +67,13 @@ class PortableTextConfig { /// Applies the custom configuration to the shared instance of the [PortableTextConfig]. apply({ - final double listIndent = 16, - final EdgeInsets itemPadding = const EdgeInsets.only(bottom: 8), + final double listIndent = defaultListIndent, + final EdgeInsets itemPadding = defaultItemPadding, final Map? styles, final Map? blockContainers, final Map? blocks, final Map? markDefs, + final TextStyle? Function(BuildContext)? baseStyle, }) { this.listIndent = listIndent; this.itemPadding = itemPadding; @@ -81,6 +81,31 @@ class PortableTextConfig { this.blockContainers.addAll(blockContainers ?? {}); this.markDefs.addAll(markDefs ?? {}); this.blocks.addAll(blocks ?? {}); + this.baseStyle = baseStyle ?? defaultBaseStyle; + } + + /// Resets the shared instance of the [PortableTextConfig] to the default configuration. + void reset() { + listIndent = defaultListIndent; + itemPadding = defaultItemPadding; + baseStyle = defaultBaseStyle; + + styles + ..clear() + ..addAll(defaultStyles); + blockContainers + ..clear() + ..addAll(defaultBlockContainers); + markDefs.clear(); + blocks + ..clear() + ..addAll(defaultBlocks); + } + + static const defaultListIndent = 16.0; + static const defaultItemPadding = EdgeInsets.only(bottom: 8); + static TextStyle? defaultBaseStyle(BuildContext context) { + return Theme.of(context).textTheme.bodyMedium; } /// The default text styles used by the shared instance of [PortableTextConfig]. diff --git a/packages/sanity/flutter_sanity_portable_text/test/portable_text_test.dart b/packages/sanity/flutter_sanity_portable_text/test/portable_text_test.dart index 52990d69..ceac6552 100644 --- a/packages/sanity/flutter_sanity_portable_text/test/portable_text_test.dart +++ b/packages/sanity/flutter_sanity_portable_text/test/portable_text_test.dart @@ -3,6 +3,10 @@ import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + setUp(() { + PortableTextConfig.shared.reset(); + }); + testWidgets('PortableText can be created with required parameters', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( @@ -71,7 +75,25 @@ void main() { testWidgets( 'PortableText shows an ErrorView when a builder for a block type is missing', (WidgetTester tester) async { - final blocks = [const MissingPortableBlockItem()]; + final blocks = [const _MissingBlockItem()]; + + await tester.pumpWidget(MaterialApp( + home: PortableText(blocks: blocks), + )); + + expect(find.byType(ErrorView), findsOneWidget); + }); + + testWidgets( + 'PortableText shows an ErrorView when a MarkDef is missing for a mark type', + (WidgetTester tester) async { + final blocks = [ + TextBlockItem( + children: [ + Span(text: 'Hello, ', marks: ['missing-mark']), + ], + ), + ]; await tester.pumpWidget(MaterialApp( home: PortableText(blocks: blocks), @@ -79,11 +101,87 @@ void main() { expect(find.byType(ErrorView), findsOneWidget); }); + + testWidgets( + 'PortableText shows an ErrorView when a builder is missing for a mark type', + (WidgetTester tester) async { + final blocks = [ + TextBlockItem( + children: [ + Span(text: 'Hello, ', marks: ['missing-key']), + ], + markDefs: [_CustomMarkDef(key: 'missing-key', color: Colors.red)], + ), + ]; + + await tester.pumpWidget(MaterialApp( + home: PortableText(blocks: blocks), + )); + + expect(find.byType(ErrorView), findsOneWidget); + }); + + // test for a custom style + testWidgets('PortableText can apply a custom style to a mark type', + (WidgetTester tester) async { + PortableTextConfig.shared.markDefs['custom-mark'] = MarkDefDescriptor( + schemaType: 'custom-mark', + fromJson: (json) => _CustomMarkDef.fromJson(json), + styleBuilder: (context, markDef, style) { + return style.apply(color: (markDef as _CustomMarkDef).color); + }, + ); + + final blocks = [ + TextBlockItem( + children: [ + Span(text: 'Hello, World', marks: ['custom-key']), + ], + markDefs: [ + _CustomMarkDef( + color: Colors.red, + key: 'custom-key', + ), + ], + ), + ]; + + await tester.pumpWidget(MaterialApp( + home: PortableText(blocks: blocks), + )); + + final richText = find.byType(RichText).evaluate().first.widget as RichText; + TextSpan? textSpan; + richText.text.visitChildren((span) { + if (span is TextSpan && span.toPlainText() == 'Hello, World') { + textSpan = span; + return false; + } + + return true; + }); + + expect(textSpan?.style?.color, Colors.red); + }); +} + +class _CustomMarkDef extends MarkDef { + final Color color; + + _CustomMarkDef({required this.color, required super.key}) + : super(type: 'custom-mark'); + + factory _CustomMarkDef.fromJson(final Map json) { + return _CustomMarkDef( + color: Color(json['color']), + key: json['_key'], + ); + } } -final class MissingPortableBlockItem implements PortableBlockItem { +final class _MissingBlockItem implements PortableBlockItem { @override - final String blockType = 'missingType'; + final String blockType = 'missing-block'; - const MissingPortableBlockItem(); + const _MissingBlockItem(); }