Testing
Rendered Markdown is mostly spans and widgets, not a collection of standalone Text nodes. Test the rendered structure and behavior rather than assuming a plain paragraph.
Find the rendered text
find.text('hello') often fails because the visible text belongs to a span tree insideRichText. Read the text from each rich-text subtree instead. Use a predicate rather thanfind.byType(RichText), because paragraphs needing inline-code or bidirectional handling can be rendered through a RichText subclass.
1String plainText(WidgetTester tester) {
2 final buffer = StringBuffer();
3 for (final richText in tester.widgetList<RichText>(
4 find.byWidgetPredicate((widget) => widget is RichText),
5 )) {
6 buffer.write(richText.text.toPlainText(includePlaceholders: false));
7 }
8 return buffer.toString();
9}
10
11testWidgets('renders Markdown text', (tester) async {
12 await tester.pumpWidget(const MaterialApp(
13 home: Scaffold(body: GptMarkdown('hello **world**')),
14 ));
15
16 expect(plainText(tester), contains('hello world'));
17})find.text still works for content deliberately rendered in a WidgetSpan—such as a custom chip, code block, or table cell—because those contain actual Text widgets. It is span content inside a Markdown paragraph that requires the helper.
Builders, patterns, and remounting
Styles, patterns, and component lists are compared and can update live. Builder closures are intentionally not compared: closures cannot be meaningfully compared without defeating the render cache. Give the widget a changing key when replacing a builder at runtime.
1// A new closure is not a meaningful rendering identity.
2// Changing it at runtime does not regenerate the parsed spans:
3await tester.pumpWidget(app(codeBuilder: builderA));
4await tester.pumpWidget(app(codeBuilder: builderB)); // still builderA
5
6// Remount when the builder identity should change:
7GptMarkdown(
8 text,
9 key: ValueKey(builderId),
10 codeBuilder: builder,
11)Text scale and expected overflow
Long code lines and long headings cannot wrap at raised text scales. Those intentional horizontal overflows are not necessarily a rendering regression. Tables and block math have their own scroll treatment. Drain the expected exception only in the focused test; do not silence exceptions globally or new overflow bugs will be hidden.
1await tester.pumpAndSettle();
2while (tester.takeException() != null) {}Streaming tests
A streaming reveal is an animation, so a normal pumpAndSettle can wait for the whole answer. Pump one frame when you are asserting an intermediate state, or disable animation when the test is about parsing or layout instead.
1// Test one frame of the reveal:
2await tester.pump(const Duration(milliseconds: 16));
3
4// Or let a finite reply finish:
5await tester.pumpAndSettle(const Duration(seconds: 5));
6
7// For a test unrelated to animation, take the static path:
8GptMarkdown(text, animation: GptMarkdownAnimation.none)
9// or:
10GptMarkdown(text, animation: GptMarkdownAnimation.fade, isStreaming: false)Test styles at the component boundary
Assert the widget that consumes a resolved style, not a screenshot pixel. Also test precedence: define one field on the app theme and another on the widget-level style sheet, then verify that both survive the merge.
1testWidgets('the quote bar follows the theme', (tester) async {
2 await tester.pumpWidget(
3 MaterialApp(
4 theme: ThemeData(extensions: [
5 GptMarkdownThemeData(
6 brightness: Brightness.light,
7 styleSheet: const GptMarkdownStyleSheet(
8 blockQuote: BlockQuoteStyle(barWidth: 7),
9 ),
10 ),
11 ]),
12 home: const Scaffold(body: GptMarkdown('> quoted')),
13 ),
14 );
15
16 final quote = tester.widget<BlockQuoteWidget>(
17 find.byType(BlockQuoteWidget),
18 );
19 expect(quote.width, 7);
20})Goldens, screenshots, and documentation snippets
Linux goldens
Default-look goldens run on Linux because text rasterization differs by platform. A tolerance was rejected because it hid real changes. Regenerate only after reviewing an intentional visual change.
gh workflow run goldens.yml --ref my-branch
gh run watch
git pullREADME images
Showcase images are documentation assets made through the widget test harness, not goldens. Regenerate them with ./scripts/screenshots.sh, commit the PNGs, and bump every README image's ?v= URL version because GitHub caches proxied images by URL.
Compiled docs
Package documentation snippets live in test/docs/snippets_test.dart and compile in the test suite, so an API rename cannot quietly leave the source guides stale. Add a snippet fixture whenever package documentation gains a new API example.
--update-goldens records the current pixels, including regressions. Inspect the expected, actual, and diff images first; on CI they are in the golden-failures artifact. Use -f commit=false when the workflow should return images as artifacts instead of committing them.
