diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml
index 7e23721..5c6fe67 100644
--- a/.github/workflows/analyze.yml
+++ b/.github/workflows/analyze.yml
@@ -131,6 +131,25 @@ jobs:
exit $EXIT
+ # Sub-packages the root excludes: each one only resolves against its
+ # own pubspec, so analyze it in place after its own pub get.
+ - name: flutter analyze sub-packages
+ if: steps.filter.outputs.any_dart == 'true'
+ run: |
+ FAILED=""
+ for pkg in hyper_render_html hyper_render_markdown hyper_render_highlight \
+ hyper_render_math hyper_render_clipboard hyper_render_devtools \
+ hyper_render_epub; do
+ echo "::group::$pkg"
+ (cd "packages/$pkg" && flutter pub get >/dev/null && \
+ flutter analyze --no-pub --fatal-warnings --fatal-infos) || FAILED="$FAILED $pkg"
+ echo "::endgroup::"
+ done
+ if [ -n "$FAILED" ]; then
+ echo "::error::analyze failed in:$FAILED"
+ exit 1
+ fi
+
- name: Upload analyze report
if: always() && steps.filter.outputs.any_dart == 'true'
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml
index 0e4f67d..6212768 100644
--- a/.github/workflows/deploy_web_demo.yml
+++ b/.github/workflows/deploy_web_demo.yml
@@ -5,10 +5,12 @@ on:
branches: [main]
workflow_dispatch:
+# Pages is served from the gh-pages branch ("Deploy from a branch"), so the
+# build is pushed there. The previous actions/deploy-pages flow needed the
+# github-pages environment to allow `main`, which it doesn't — every run
+# failed and the live demo stayed on v1.7.1.
permissions:
- contents: read
- pages: write
- id-token: write
+ contents: write
concurrency:
group: 'pages'
@@ -16,9 +18,6 @@ concurrency:
jobs:
deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -40,11 +39,11 @@ jobs:
cd example
flutter build web --release --base-href "/hyper_render/"
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
+ - name: Publish to gh-pages
+ uses: peaceiris/actions-gh-pages@v4
with:
- path: example/build/web
-
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: example/build/web
+ publish_branch: gh-pages
+ force_orphan: true # keep gh-pages to a single commit, no history bloat
+ commit_message: "deploy: live web demo"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7915405..ad4d6a1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -40,6 +40,8 @@ jobs:
changed_highlight: ${{ steps.filter.outputs.highlight }}
changed_clipboard: ${{ steps.filter.outputs.clipboard }}
changed_devtools: ${{ steps.filter.outputs.devtools }}
+ changed_math: ${{ steps.filter.outputs.math }}
+ changed_epub: ${{ steps.filter.outputs.epub }}
changed_root: ${{ steps.filter.outputs.root }}
changed_any_dart: ${{ steps.filter.outputs.any_dart }}
steps:
@@ -60,6 +62,10 @@ jobs:
- 'packages/hyper_render_clipboard/**'
devtools:
- 'packages/hyper_render_devtools/**'
+ math:
+ - 'packages/hyper_render_math/**'
+ epub:
+ - 'packages/hyper_render_epub/**'
root:
- 'lib/**'
- 'test/**'
@@ -99,45 +105,12 @@ jobs:
working-directory: example/android
run: ./gradlew assembleDebug
- emulator-tests:
- name: Integration Tests (Emulator)
- runs-on: macos-latest
- needs: path-filter
- if: >-
- github.event_name == 'pull_request' &&
- needs.path-filter.outputs.changed_any_dart == 'true'
- strategy:
- fail-fast: false
- matrix:
- platform: [android, ios]
- steps:
- - uses: actions/checkout@v4
- - name: Setup Java
- if: matrix.platform == 'android'
- uses: actions/setup-java@v3
- with:
- distribution: 'zulu'
- java-version: '17'
- - name: Setup Flutter
- uses: subosito/flutter-action@v2
- with:
- flutter-version: ${{ env.FLUTTER_VERSION }}
- channel: stable
- cache: true
- - name: flutter pub get
- run: flutter pub get
- - name: Run Android Emulator Tests
- if: matrix.platform == 'android'
- uses: reactivecircus/android-emulator-runner@v2
- with:
- api-level: 29
- arch: arm64-v8a
- script: flutter test test/integration/
- - name: Run iOS Simulator Tests
- if: matrix.platform == 'ios'
- run: |
- xcrun simctl list devicetypes
- flutter test test/integration/ -d "iPhone 15" || flutter test test/integration/
+ # NOTE: the former "Integration Tests (Emulator)" job was removed. It ran
+ # `flutter test test/integration/` with no device target, i.e. on the host VM
+ # — the same tests test-pr runs below — but on macOS runners (10x the
+ # per-minute price), and its Android leg could not boot an x86 emulator on
+ # Apple Silicon (HVF_UNSUPPORTED). Real on-device coverage lives in
+ # example/integration_test/all_demos_test.dart (run locally with -d macos).
test-pr:
name: Tests (PR · ubuntu-22.04 · stable)
@@ -190,7 +163,7 @@ jobs:
needs.path-filter.outputs.changed_html == 'true' ||
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_html
- run: flutter test --no-pub
+ run: flutter pub get && flutter test --no-pub
- name: Test hyper_render_markdown
if: >-
@@ -198,7 +171,7 @@ jobs:
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_markdown
run: |
- if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi
+ if [ -d test ]; then flutter pub get && flutter test --no-pub; else echo "No tests in this package — skipping."; fi
- name: Test hyper_render_highlight
if: >-
@@ -206,7 +179,7 @@ jobs:
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_highlight
run: |
- if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi
+ if [ -d test ]; then flutter pub get && flutter test --no-pub; else echo "No tests in this package — skipping."; fi
- name: Test hyper_render_clipboard
if: >-
@@ -214,7 +187,36 @@ jobs:
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_clipboard
run: |
- if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi
+ if [ -d test ]; then flutter pub get && flutter test --no-pub; else echo "No tests in this package — skipping."; fi
+
+ - name: Test hyper_render_math
+ if: >-
+ needs.path-filter.outputs.changed_math == 'true' ||
+ needs.path-filter.outputs.changed_core == 'true'
+ working-directory: packages/hyper_render_math
+ run: flutter pub get && flutter test --no-pub
+
+ - name: Test hyper_render_epub
+ if: >-
+ needs.path-filter.outputs.changed_epub == 'true' ||
+ needs.path-filter.outputs.changed_root == 'true' ||
+ needs.path-filter.outputs.changed_core == 'true'
+ working-directory: packages/hyper_render_epub
+ run: flutter pub get && flutter test --no-pub
+
+ - name: Test hyper_render_devtools
+ if: >-
+ needs.path-filter.outputs.changed_devtools == 'true' ||
+ needs.path-filter.outputs.changed_core == 'true'
+ working-directory: packages/hyper_render_devtools
+ run: flutter pub get && flutter test --no-pub
+
+ # The panel imports dart:js_interop: its widget test only compiles for
+ # the web (@TestOn('browser')). ubuntu runners ship Chrome.
+ - name: Test hyper_render_devtools panel (Chrome)
+ if: needs.path-filter.outputs.changed_devtools == 'true'
+ working-directory: packages/hyper_render_devtools/devtools_ui
+ run: flutter pub get && flutter test --no-pub --platform chrome
- name: Upload test results
if: failure()
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 100454e..818b8be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## 1.10.0
+
+- **`var()` in `
(find.byType(HyperRenderWidget))) {
+ UDTNode? p;
+ void walk(UDTNode n) {
+ if (n.tagName == 'p') p ??= n;
+ n.children.forEach(walk);
+ }
+
+ walk(w.document);
+ if (p != null) return p!.style.color;
+ }
+ fail('no rendered');
+ }
+
+ testWidgets('sync mode re-renders with an overridden variable',
+ (tester) async {
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(body: HyperViewer(html: html, mode: HyperRenderMode.sync)),
+ ));
+ await tester.pumpAndSettle();
+ expect(paragraphColor(tester), const Color(0xFFFF0000));
+
+ HyperRenderDebugHooks.cssVariableOverrides.value = {'--brand': '#0000ff'};
+ await tester.pumpAndSettle();
+ expect(paragraphColor(tester), const Color(0xFF0000FF));
+
+ HyperRenderDebugHooks.cssVariableOverrides.value = const {};
+ await tester.pumpAndSettle();
+ expect(paragraphColor(tester), const Color(0xFFFF0000));
+ });
+
+ testWidgets('virtualized (async parse) mode picks up overrides too',
+ (tester) async {
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(
+ body: HyperViewer(
+ html: html,
+ mode: HyperRenderMode.virtualized,
+ renderConfig: HyperRenderConfig(useMicrotaskParsing: true),
+ ),
+ ),
+ ));
+ await tester.pumpAndSettle();
+ expect(paragraphColor(tester), const Color(0xFFFF0000));
+
+ HyperRenderDebugHooks.cssVariableOverrides.value = {'--brand': '#00ff00'};
+ await tester.pumpAndSettle();
+ expect(paragraphColor(tester), const Color(0xFF00FF00));
+ });
+
+ testWidgets('compute() isolate parse receives the overrides', (tester) async {
+ // Default native config parses virtualized HTML in a real isolate,
+ // where HyperRenderDebugHooks' statics are NOT shared — the overrides
+ // must travel in the args record. Isolates don't run under FakeAsync.
+ HyperRenderDebugHooks.cssVariableOverrides.value = {'--brand': '#00ff00'};
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(
+ body: HyperViewer(
+ html: html,
+ mode: HyperRenderMode.virtualized,
+ renderConfig: HyperRenderConfig(useMicrotaskParsing: false),
+ ),
+ ),
+ ));
+ for (var i = 0;
+ i < 50 && find.byType(HyperRenderWidget).evaluate().isEmpty;
+ i++) {
+ await tester.runAsync(
+ () => Future.delayed(const Duration(milliseconds: 20)));
+ await tester.pump();
+ }
+ await tester.pump(const Duration(milliseconds: 400));
+ expect(paragraphColor(tester), const Color(0xFF00FF00));
+ });
+
+ testWidgets('a disposed viewer stops listening', (tester) async {
+ await tester.pumpWidget(const MaterialApp(
+ home: Scaffold(body: HyperViewer(html: html, mode: HyperRenderMode.sync)),
+ ));
+ await tester.pumpAndSettle();
+ await tester.pumpWidget(const SizedBox());
+ HyperRenderDebugHooks.cssVariableOverrides.value = {'--brand': '#0000ff'};
+ await tester.pump();
+ expect(tester.takeException(), isNull);
+ });
+}
diff --git a/test/fuzz/parser_fuzz_test.dart b/test/fuzz/parser_fuzz_test.dart
index 329a867..42385a2 100644
--- a/test/fuzz/parser_fuzz_test.dart
+++ b/test/fuzz/parser_fuzz_test.dart
@@ -161,6 +161,11 @@ void main() {
'```\n```\n```\n',
'[[[[[[[[[[text]]]]]]]]]]',
'***___***text***___***',
+ // Stylesheet var()/url()/calc() shapes — x
',
+ 'x',
+ '
x
',
+ 'x
',
];
for (var i = 0; i < adversarial.length; i++) {
diff --git a/test/golden/goldens/blockquote.png b/test/golden/goldens/blockquote.png
index ec0e782..4e9de71 100644
Binary files a/test/golden/goldens/blockquote.png and b/test/golden/goldens/blockquote.png differ
diff --git a/test/golden/goldens/canvas_block_animation_boxshadow.png b/test/golden/goldens/canvas_block_animation_boxshadow.png
index db7cb43..da460fa 100644
Binary files a/test/golden/goldens/canvas_block_animation_boxshadow.png and b/test/golden/goldens/canvas_block_animation_boxshadow.png differ
diff --git a/test/golden/goldens/canvas_block_animation_slidein_50pct.png b/test/golden/goldens/canvas_block_animation_slidein_50pct.png
index d4906fd..3eedd1f 100644
Binary files a/test/golden/goldens/canvas_block_animation_slidein_50pct.png and b/test/golden/goldens/canvas_block_animation_slidein_50pct.png differ
diff --git a/test/golden/goldens/cjk_kinsoku.png b/test/golden/goldens/cjk_kinsoku.png
index e8a145f..f247cdf 100644
Binary files a/test/golden/goldens/cjk_kinsoku.png and b/test/golden/goldens/cjk_kinsoku.png differ
diff --git a/test/golden/goldens/cjk_ruby.png b/test/golden/goldens/cjk_ruby.png
index e6cc16d..8d6261a 100644
Binary files a/test/golden/goldens/cjk_ruby.png and b/test/golden/goldens/cjk_ruby.png differ
diff --git a/test/golden/goldens/code_block.png b/test/golden/goldens/code_block.png
index 56ac042..1e0565a 100644
Binary files a/test/golden/goldens/code_block.png and b/test/golden/goldens/code_block.png differ
diff --git a/test/golden/goldens/definition_list.png b/test/golden/goldens/definition_list.png
index a512127..549ceee 100644
Binary files a/test/golden/goldens/definition_list.png and b/test/golden/goldens/definition_list.png differ
diff --git a/test/golden/goldens/float_cjk.png b/test/golden/goldens/float_cjk.png
index 791f9e0..484422f 100644
Binary files a/test/golden/goldens/float_cjk.png and b/test/golden/goldens/float_cjk.png differ
diff --git a/test/golden/goldens/float_clear.png b/test/golden/goldens/float_clear.png
index 446afc3..78600d0 100644
Binary files a/test/golden/goldens/float_clear.png and b/test/golden/goldens/float_clear.png differ
diff --git a/test/golden/goldens/float_left.png b/test/golden/goldens/float_left.png
index 7b5efbc..abdefaa 100644
Binary files a/test/golden/goldens/float_left.png and b/test/golden/goldens/float_left.png differ
diff --git a/test/golden/goldens/float_right.png b/test/golden/goldens/float_right.png
index 4fe27f0..40b825e 100644
Binary files a/test/golden/goldens/float_right.png and b/test/golden/goldens/float_right.png differ
diff --git a/test/golden/goldens/full_article.png b/test/golden/goldens/full_article.png
index 5d24796..9b4e2a8 100644
Binary files a/test/golden/goldens/full_article.png and b/test/golden/goldens/full_article.png differ
diff --git a/test/golden/goldens/headings.png b/test/golden/goldens/headings.png
index 315a14c..451a454 100644
Binary files a/test/golden/goldens/headings.png and b/test/golden/goldens/headings.png differ
diff --git a/test/golden/goldens/image_placeholder.png b/test/golden/goldens/image_placeholder.png
index 64c4d30..9ae7316 100644
Binary files a/test/golden/goldens/image_placeholder.png and b/test/golden/goldens/image_placeholder.png differ
diff --git a/test/golden/goldens/links.png b/test/golden/goldens/links.png
index f6b3c7e..761a71d 100644
Binary files a/test/golden/goldens/links.png and b/test/golden/goldens/links.png differ
diff --git a/test/golden/goldens/mixed_inline.png b/test/golden/goldens/mixed_inline.png
index fce73e4..f26d19c 100644
Binary files a/test/golden/goldens/mixed_inline.png and b/test/golden/goldens/mixed_inline.png differ
diff --git a/test/golden/goldens/mixed_lists.png b/test/golden/goldens/mixed_lists.png
index c43bc54..c86d580 100644
Binary files a/test/golden/goldens/mixed_lists.png and b/test/golden/goldens/mixed_lists.png differ
diff --git a/test/golden/goldens/ordered_list.png b/test/golden/goldens/ordered_list.png
index 0b8d808..f12e7c6 100644
Binary files a/test/golden/goldens/ordered_list.png and b/test/golden/goldens/ordered_list.png differ
diff --git a/test/golden/goldens/rtl_arabic.png b/test/golden/goldens/rtl_arabic.png
index 1ec5f64..b0c56b0 100644
Binary files a/test/golden/goldens/rtl_arabic.png and b/test/golden/goldens/rtl_arabic.png differ
diff --git a/test/golden/goldens/rtl_hebrew.png b/test/golden/goldens/rtl_hebrew.png
index 096633c..a2a74cb 100644
Binary files a/test/golden/goldens/rtl_hebrew.png and b/test/golden/goldens/rtl_hebrew.png differ
diff --git a/test/golden/goldens/rtl_mixed.png b/test/golden/goldens/rtl_mixed.png
index bce4fa6..23fb9dd 100644
Binary files a/test/golden/goldens/rtl_mixed.png and b/test/golden/goldens/rtl_mixed.png differ
diff --git a/test/golden/goldens/table.png b/test/golden/goldens/table.png
index a118f8f..8babef5 100644
Binary files a/test/golden/goldens/table.png and b/test/golden/goldens/table.png differ
diff --git a/test/golden/goldens/table_border_spacing_collapse.png b/test/golden/goldens/table_border_spacing_collapse.png
index f3e8153..a6ac565 100644
Binary files a/test/golden/goldens/table_border_spacing_collapse.png and b/test/golden/goldens/table_border_spacing_collapse.png differ
diff --git a/test/golden/goldens/table_border_spacing_separate.png b/test/golden/goldens/table_border_spacing_separate.png
index f5a9cbc..872d3bb 100644
Binary files a/test/golden/goldens/table_border_spacing_separate.png and b/test/golden/goldens/table_border_spacing_separate.png differ
diff --git a/test/golden/goldens/table_spans.png b/test/golden/goldens/table_spans.png
index dd1c7c0..7965e85 100644
Binary files a/test/golden/goldens/table_spans.png and b/test/golden/goldens/table_spans.png differ
diff --git a/test/golden/goldens/text_formatting.png b/test/golden/goldens/text_formatting.png
index cf953ac..245aaa3 100644
Binary files a/test/golden/goldens/text_formatting.png and b/test/golden/goldens/text_formatting.png differ
diff --git a/test/golden/goldens/unordered_list.png b/test/golden/goldens/unordered_list.png
index fb0e046..f4f7228 100644
Binary files a/test/golden/goldens/unordered_list.png and b/test/golden/goldens/unordered_list.png differ
diff --git a/test/integration/advanced_security_test.dart b/test/integration/advanced_security_test.dart
index 52cd964..5f672b6 100644
--- a/test/integration/advanced_security_test.dart
+++ b/test/integration/advanced_security_test.dart
@@ -73,6 +73,51 @@ void main() {
expect(tester.takeException(), isNull);
});
+ // Survives
';
+ final sw = Stopwatch()..start();
+ await tester.pumpWidget(MaterialApp(
+ home:
+ Scaffold(body: HyperViewer(html: html, mode: HyperRenderMode.sync)),
+ ));
+ await tester.pumpAndSettle();
+ expect(sw.elapsed, lessThan(const Duration(seconds: 5)));
+ expect(tester.takeException(), isNull);
+ expect(find.byType(HyperRenderWidget), findsWidgets);
+ });
+
+ testWidgets('Dangerous url() schemes in ';
+ await tester.pumpWidget(const MaterialApp(
+ home:
+ Scaffold(body: HyperViewer(html: html, mode: HyperRenderMode.sync)),
+ ));
+ await tester.pumpAndSettle();
+ final doc = tester
+ .widget(find.byType(HyperRenderWidget).first)
+ .document;
+ final images = [];
+ void walk(UDTNode n) {
+ images.add(n.style.backgroundImage);
+ n.children.forEach(walk);
+ }
+
+ walk(doc);
+ expect(images.whereType(), isEmpty);
+ });
+
testWidgets(
'Ensures allowedTags doesn\'t accidentally allow dangerous tags',
(tester) async {
diff --git a/test/paint_paths_pixel_test.dart b/test/paint_paths_pixel_test.dart
new file mode 100644
index 0000000..3903059
--- /dev/null
+++ b/test/paint_paths_pixel_test.dart
@@ -0,0 +1,172 @@
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hyper_render/hyper_render.dart';
+
+/// Pixel-level checks for RenderHyperBox paint paths that "does not throw"
+/// tests cannot see: each case renders, rasterizes, and counts pixels of
+/// the colour that path is supposed to draw.
+void main() {
+ final boundaryKey = GlobalKey();
+
+ Future render(
+ WidgetTester tester,
+ String html, {
+ bool debugBounds = false,
+ void Function(RenderHyperBox box)? before,
+ }) async {
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ backgroundColor: Colors.white,
+ body: RepaintBoundary(
+ key: boundaryKey,
+ child: SizedBox(
+ width: 400,
+ height: 300,
+ child: HyperViewer(
+ html: html,
+ mode: HyperRenderMode.sync,
+ selectable: true,
+ debugShowHyperRenderBounds: debugBounds,
+ selectionColor: const Color(0xFF00FF00),
+ ),
+ ),
+ ),
+ ),
+ ));
+ await tester.pumpAndSettle();
+ if (before != null) {
+ before(tester
+ .renderObject(find.byType(HyperRenderWidget).first));
+ await tester.pumpAndSettle();
+ }
+ final boundary = boundaryKey.currentContext!.findRenderObject()!
+ as RenderRepaintBoundary;
+ late ui.Image image;
+ await tester.runAsync(() async {
+ image = await boundary.toImage();
+ });
+ return image;
+ }
+
+ /// Pixels within [tolerance] per channel of [color] (alpha ignored).
+ /// The boundary has no background, so translucent paints keep their own
+ /// RGB rather than blending toward white.
+ Future countPixels(WidgetTester tester, ui.Image image, Color color,
+ {int tolerance = 40}) async {
+ late ByteData data;
+ await tester.runAsync(() async {
+ data =
+ (await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba))!;
+ });
+ final r = (color.r * 255).round(),
+ g = (color.g * 255).round(),
+ b = (color.b * 255).round();
+ var n = 0;
+ for (var i = 0; i < data.lengthInBytes; i += 4) {
+ if ((data.getUint8(i) - r).abs() <= tolerance &&
+ (data.getUint8(i + 1) - g).abs() <= tolerance &&
+ (data.getUint8(i + 2) - b).abs() <= tolerance) {
+ n++;
+ }
+ }
+ return n;
+ }
+
+ const red = Color(0xFFFF0000);
+
+ for (final style in ['solid', 'dashed', 'dotted', 'double']) {
+ testWidgets('block border-style: $style paints the border colour',
+ (tester) async {
+ final image = await render(
+ tester,
+ ''
+ 'Bordered
');
+ final borderPixels = await countPixels(tester, image, red);
+ // A 6px border around a ~400px-wide box: hundreds of red pixels even
+ // for dotted/dashed (which leave gaps) — none would mean not painted.
+ expect(borderPixels, greaterThan(200), reason: style);
+ });
+ }
+
+ testWidgets('dashed leaves gaps a solid border does not', (tester) async {
+ final solid = await countPixels(
+ tester,
+ await render(tester,
+ 'x
'),
+ red);
+ final dashed = await countPixels(
+ tester,
+ await render(tester,
+ 'x
'),
+ red);
+ expect(dashed, lessThan(solid));
+ });
+
+ testWidgets('block background and rounded border', (tester) async {
+ final image = await render(
+ tester,
+ 'Box
');
+ expect(await countPixels(tester, image, const Color(0xFF00FF00)),
+ greaterThan(2000));
+ expect(await countPixels(tester, image, const Color(0xFF0000FF)),
+ greaterThan(100));
+ });
+
+ testWidgets('inline background and border on a span', (tester) async {
+ // Padding: the test font (Ahem) draws glyphs as solid em boxes, which
+ // would hide a background drawn at exactly the glyph box.
+ final image = await render(
+ tester,
+ 'Before highlighted after
');
+ expect(await countPixels(tester, image, const Color(0xFFFFFF00)),
+ greaterThan(100));
+ expect(await countPixels(tester, image, const Color(0xFFFF00FF)),
+ greaterThan(100));
+ });
+
+ testWidgets('debugShowHyperRenderBounds outlines line rows', (tester) async {
+ const html = 'Debug bounds paint outlines
';
+ final plain = await render(tester, html);
+ final debug = await render(tester, html, debugBounds: true);
+ // Line rows are outlined in 0x66007BFF. (Fragment outlines sit under
+ // the solid test-font glyphs, so count the line colour.)
+ const lineBlue = Color(0xFF007BFF);
+ expect(await countPixels(tester, plain, lineBlue, tolerance: 10), 0);
+ expect(await countPixels(tester, debug, lineBlue, tolerance: 10),
+ greaterThan(300));
+ });
+
+ testWidgets('selection highlight is painted behind selected text',
+ (tester) async {
+ // Transparent text: solid test-font glyphs would cover the highlight.
+ const html =
+ 'Select all of this text please
';
+ final before = await render(tester, html);
+ final after = await render(tester, html, before: (b) => b.selectAll());
+ const green = Color(0xFF00FF00); // selectionColor passed to the viewer
+ final c0 = await countPixels(tester, before, green, tolerance: 10);
+ final c1 = await countPixels(tester, after, green, tolerance: 10);
+ expect(c0, 0);
+ expect(c1, greaterThan(1000));
+ });
+
+ testWidgets('a float with an unloaded image paints its placeholder',
+ (tester) async {
+ final image = await render(
+ tester,
+ '
Text flows beside the float.
');
+ // The placeholder is a light-grey box; plain white would mean nothing
+ // was drawn where the float sits.
+ expect(
+ await countPixels(tester, image, const Color(0xFFEEEEEE),
+ tolerance: 18),
+ greaterThan(500));
+ });
+}