Skip to content

Commit

Permalink
chore: adding more tests, fixing some interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
pavanpodila committed Apr 4, 2024
1 parent d2da593 commit db97ee0
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <MarkDef>[];

// Step 2: Accumulate the styles across all marks
for (final mark in span.marks) {
/// Standard marks (aka annotations)
final builder = config.styles[mark];
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,14 +51,13 @@ class PortableTextConfig {
final Map<String, MarkDefDescriptor> 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.
Expand All @@ -68,19 +67,45 @@ 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<String, TextStyleBuilder>? styles,
final Map<String, BlockContainerBuilder>? blockContainers,
final Map<String, BlockWidgetBuilder>? blocks,
final Map<String, MarkDefDescriptor>? markDefs,
final TextStyle? Function(BuildContext)? baseStyle,
}) {
this.listIndent = listIndent;
this.itemPadding = itemPadding;
this.styles.addAll(styles ?? {});
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].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -71,19 +75,113 @@ 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),
));

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<String, dynamic> 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();
}

0 comments on commit db97ee0

Please sign in to comment.