import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:clide/src/svg/svg_document.dart'; import 'package:clide/widgets/src/svg/svg_painter.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; /// Pixel-probe tests: render the scene to a `ui.Image` and read back colours. /// Deterministic for fills, independent of fonts/goldens. void main() { TestWidgetsFlutterBinding.ensureInitialized(); Future render(String svg, double w, double h, {SvgImageResolver? images}) async { final recorder = ui.PictureRecorder(); final canvas = ui.Canvas(recorder, Rect.fromLTWH(0, 0, w, h)); paintSvg(canvas, Size(w, h), buildSvgDocument(svg), images: images); return recorder.endRecording().toImage(w.round(), h.round()); } Future solidImage(int w, int h, int argb) { final px = Uint8List(w * h * 4); for (var i = 0; i < w * h; i++) { px[i * 4] = (argb >> 16) & 0xFF; px[i * 4 + 1] = (argb >> 8) & 0xFF; px[i * 4 + 2] = argb & 0xFF; px[i * 4 + 3] = (argb >> 24) & 0xFF; } final c = Completer(); ui.decodeImageFromPixels(px, w, h, ui.PixelFormat.rgba8888, c.complete); return c.future; } Future argbAt(ui.Image img, int x, int y) async { final data = (await img.toByteData())!; final i = (y * img.width + x) * 4; return (data.getUint8(i + 3) << 24) | (data.getUint8(i) << 16) | (data.getUint8(i + 1) << 8) | data.getUint8(i + 2); } int alpha(int argb) => (argb >> 24) & 0xFF; test('fills a rect with its colour', () async { final img = await render('', 10, 10); expect(await argbAt(img, 5, 5), 0xFFFF0000); }); test('default fill is black', () async { final img = await render('', 10, 10); expect(await argbAt(img, 5, 5), 0xFF000000); }); test('fill="none" leaves the interior transparent', () async { final img = await render('', 10, 10); expect(alpha(await argbAt(img, 5, 5)), 0); }); test('circle centre is filled, corner is empty', () async { final img = await render('', 10, 10); expect(await argbAt(img, 5, 5), 0xFF0000FF); expect(alpha(await argbAt(img, 0, 0)), 0); }); test('viewBox scales up to the paint size', () async { final img = await render('', 20, 20); expect(await argbAt(img, 10, 10), 0xFFFF0000); }); test('transform translates the shape', () async { final img = await render('', 20, 20); expect(await argbAt(img, 12, 12), 0xFFFF0000); expect(alpha(await argbAt(img, 2, 2)), 0); }); test('group opacity dims the contents', () async { final img = await render('', 10, 10); expect(alpha(await argbAt(img, 5, 5)), closeTo(128, 4)); }); test('draws a marker-end arrowhead, rotated to the path direction', () async { // A rightward path with a green right-pointing triangle marker at its end. const svg = '' '' '' ''; final data = (await (await render(svg, 40, 20)).toByteData())!; var greenDrawn = false; for (var i = 0; i < data.lengthInBytes; i += 4) { final r = data.getUint8(i), g = data.getUint8(i + 1), b = data.getUint8(i + 2), a = data.getUint8(i + 3); if (a > 200 && g > 200 && r < 80 && b < 80) { greenDrawn = true; break; } } expect(greenDrawn, isTrue, reason: 'the green arrowhead should have been painted'); }); test('paints an via the injected resolver, into its dest rect', () async { final pic = await solidImage(4, 4, 0xFFFF00FF); const svg = ''; final img = await render(svg, 10, 10, images: (href) => href == 'pic' ? pic : null); expect(await argbAt(img, 5, 5), 0xFFFF00FF); }); test('an with no resolver paints nothing', () async { const svg = ''; final img = await render(svg, 10, 10); expect(alpha(await argbAt(img, 5, 5)), 0); }); test('a wide image is aspect-fit (contain) into a square cell, centered (T-319)', () async { // 8x2 source into a 10x10 cell → scaled to 10x2.5, centered vertically // (rows ~3.75–6.25). The center has the image; the top/bottom margins stay // empty — proving contain-fit, not a stretched fill. final wide = await solidImage(8, 2, 0xFF00FF00); const svg = ''; final img = await render(svg, 10, 10, images: (href) => href == 'pic' ? wide : null); expect(await argbAt(img, 5, 5), 0xFF00FF00); // center: image expect(alpha(await argbAt(img, 5, 0)), 0); // top margin: empty expect(alpha(await argbAt(img, 5, 9)), 0); // bottom margin: empty }); test('renders the real d2 fixture without error and draws ink', () async { final svg = File('test/svg/fixtures/d2_pipeline.svg').readAsStringSync(); final img = await render(svg, 200, 120); final data = (await img.toByteData())!; var anyInk = false; for (var i = 3; i < data.lengthInBytes; i += 4) { if (data.getUint8(i) != 0) { anyInk = true; break; } } expect(anyInk, isTrue); }); const markerDefs = ''; test('renders quad/arc/close path segments with markers (T-320)', () async { // Each path leads with a different first-draw op to exercise every // start-angle branch in the marker placement + the segPath builder. for (final d in ['M5 25 Q20 5 35 25 Z', 'M5 25 A8 8 0 0 1 50 25', 'M5 25 Z']) { final svg = '$markerDefs'; final img = await render(svg, 100, 50); expect(img.width, 100); } }); test('a line carries start + end markers (T-320)', () async { final svg = '$markerDefs'; final img = await render(svg, 50, 50); expect(img.width, 50); }); test('renders text anchors/baselines/weights + a fill-opacity (T-320)', () async { const svg = '' '' 'M' 'e' ''; final img = await render(svg, 40, 20); expect(img.width, 40); // exercised the anchor/baseline/weight + fill-opacity color paths }); }