diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbd666..942d46c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.9.5] - 2026-05-12 + +### Fixed +- **Dart agent: iOS / Impeller screenshots** — `take_screenshot` previously called `OffsetLayer.toImage()` on the root render view. On iOS with the Impeller renderer (Flutter's default on iOS 17+), that returns a GPU-backed texture whose `toByteData(ImageByteFormat.png)` is `null`, so capture silently produced nothing. The agent now primarily captures via the largest visible `RenderRepaintBoundary` in the widget tree (Impeller-supported); the legacy `OffsetLayer` path is only used as a fallback when no boundary is found (Skia). Also awaits `WidgetsBinding.instance.endOfFrame` before capture so the latest frame is always in the image, and uses the actual `View.devicePixelRatio` rather than a hard-coded `2.0`. + ## [0.9.4] - 2026-05-09 ### Added diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 1b5b3f9..9aee097 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -18,7 +18,7 @@ Welcome to the FlutterProbe wiki. This documentation covers architecture details ## Project Status -FlutterProbe is in active development. Current version: **0.9.4**. +FlutterProbe is in active development. Current version: **0.9.5**. ### Repository Structure diff --git a/probe_agent/CHANGELOG.md b/probe_agent/CHANGELOG.md index 97ee7c3..4b8819e 100644 --- a/probe_agent/CHANGELOG.md +++ b/probe_agent/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.9.5 - 2026-05-12 + +- **Fix: iOS/Impeller screenshots** — `take_screenshot` previously called + `OffsetLayer.toImage()` on the root render view, which on iOS with the + Impeller renderer returns a GPU-backed texture whose `toByteData(png)` + is `null` — silently breaking screenshot capture. The agent now + primarily captures via the largest visible `RenderRepaintBoundary` in + the widget tree (Impeller-supported), and falls back to the old + `OffsetLayer.toImage()` path only when no boundary is found (Skia). + Awaits `WidgetsBinding.instance.endOfFrame` before capture so the + latest frame is always in the image. Uses the actual view's + `devicePixelRatio` rather than a hard-coded `2.0`. + ## 0.9.4 - 2026-05-09 - Version bump to match CLI v0.9.4. No agent code changes — the .mcpb diff --git a/probe_agent/lib/src/executor.dart b/probe_agent/lib/src/executor.dart index 6c18c78..e4021a8 100644 --- a/probe_agent/lib/src/executor.dart +++ b/probe_agent/lib/src/executor.dart @@ -648,26 +648,68 @@ class ProbeExecutor { // ---- Screenshot ---- Future _screenshot(String name) async { - // ignore: deprecated_member_use - final renderView = WidgetsBinding.instance.renderView; - // ignore: invalid_use_of_protected_member - final layer = renderView.layer; - if (layer == null || layer is! OffsetLayer) { - throw ProbeError(ProbeError.internalError, 'No renderable layer for screenshot'); + // Wait for the latest frame to be fully rendered before capturing. + await WidgetsBinding.instance.endOfFrame; + + // Primary path: RenderRepaintBoundary.toImage() — works on both Skia and + // Impeller. OffsetLayer.toImage() returns a GPU-backed texture on Impeller + // where toByteData(png) returns null, so we can't rely on it for iOS. + final pngBytes = await _captureViaRepaintBoundary() ?? await _captureViaLayer(); + if (pngBytes == null) { + throw ProbeError(ProbeError.internalError, 'Screenshot capture failed: no renderable surface'); } - final image = await layer.toImage( - renderView.paintBounds, - pixelRatio: 2.0, - ); - final bytes = await image.toByteData(format: ui.ImageByteFormat.png); + final dir = '${Directory.systemTemp.path}/probe_screenshots'; final path = '$dir/${name}_${DateTime.now().millisecondsSinceEpoch}.png'; final file = File(path); await file.parent.create(recursive: true); - await file.writeAsBytes(bytes!.buffer.asUint8List()); + await file.writeAsBytes(pngBytes); return path; } + /// Finds the largest [RenderRepaintBoundary] in the widget tree and captures + /// it. Impeller explicitly supports this path, unlike [OffsetLayer.toImage]. + Future _captureViaRepaintBoundary() async { + RenderRepaintBoundary? best; + double bestArea = 0; + + void visit(Element element) { + final ro = element.renderObject; + if (ro is RenderRepaintBoundary) { + final area = ro.size.width * ro.size.height; + if (area > bestArea && ro.size.width > 50) { + bestArea = area; + best = ro; + } + } + element.visitChildren(visit); + } + + WidgetsBinding.instance.rootElement?.visitChildren(visit); + if (best == null) return null; + + final views = RendererBinding.instance.renderViews; + final pixelRatio = views.isNotEmpty + ? views.first.flutterView.devicePixelRatio + : ui.PlatformDispatcher.instance.views.first.devicePixelRatio; + final image = await best!.toImage(pixelRatio: pixelRatio); + final bytes = await image.toByteData(format: ui.ImageByteFormat.png); + return bytes?.buffer.asUint8List(); + } + + /// Fallback capture using the root [OffsetLayer]. Works on Skia; may return + /// null on Impeller if the GPU texture can't be read back to CPU memory. + Future _captureViaLayer() async { + // ignore: deprecated_member_use + final renderView = WidgetsBinding.instance.renderView; + // ignore: invalid_use_of_protected_member + final layer = renderView.layer; + if (layer == null || layer is! OffsetLayer) return null; + final image = await layer.toImage(renderView.paintBounds, pixelRatio: 2.0); + final bytes = await image.toByteData(format: ui.ImageByteFormat.png); + return bytes?.buffer.asUint8List(); + } + // ---- Open link ---- Future _openLink(String url) async { diff --git a/probe_agent/pubspec.yaml b/probe_agent/pubspec.yaml index ec215a9..47fcea3 100644 --- a/probe_agent/pubspec.yaml +++ b/probe_agent/pubspec.yaml @@ -3,7 +3,7 @@ description: >- On-device E2E test agent for FlutterProbe. Embeds in your Flutter app and executes test commands via direct widget-tree access with sub-50ms latency. -version: 0.9.4 +version: 0.9.5 homepage: https://flutterprobe.dev repository: https://github.com/AlphaWaveSystems/flutter-probe issue_tracker: https://github.com/AlphaWaveSystems/flutter-probe/issues diff --git a/probe_annotation/CHANGELOG.md b/probe_annotation/CHANGELOG.md index e8f8bb8..54d7ca9 100644 --- a/probe_annotation/CHANGELOG.md +++ b/probe_annotation/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.5 - 2026-05-12 + +- Version bump to match CLI v0.9.5. No annotation API changes. + ## 0.9.4 - 2026-05-09 - Version bump to match CLI v0.9.4. No annotation API changes. diff --git a/probe_annotation/pubspec.yaml b/probe_annotation/pubspec.yaml index 0fe9d57..50ad651 100644 --- a/probe_annotation/pubspec.yaml +++ b/probe_annotation/pubspec.yaml @@ -3,7 +3,7 @@ description: >- Dart annotations for FlutterProbe — declare ProbeScript end-to-end tests as decorators on your Flutter screen classes. Pair with flutter_probe_gen to generate .probe test files at build time. -version: 0.9.4 +version: 0.9.5 homepage: https://flutterprobe.dev repository: https://github.com/AlphaWaveSystems/flutter-probe issue_tracker: https://github.com/AlphaWaveSystems/flutter-probe/issues diff --git a/probe_gen/CHANGELOG.md b/probe_gen/CHANGELOG.md index e8521e1..48ea871 100644 --- a/probe_gen/CHANGELOG.md +++ b/probe_gen/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.9.5 - 2026-05-12 + +- Version bump to match CLI v0.9.5. No annotation API changes. + ## 0.9.4 - 2026-05-09 - Version bump to match CLI v0.9.4. No annotation API changes. diff --git a/probe_gen/pubspec.yaml b/probe_gen/pubspec.yaml index 40fdd52..e570f44 100644 --- a/probe_gen/pubspec.yaml +++ b/probe_gen/pubspec.yaml @@ -4,7 +4,7 @@ description: >- flutter_probe_annotation decorators on Flutter classes and emits .probe test files into tests/generated/. Pair with flutter_probe_agent and the probe CLI to run the generated end-to-end tests. -version: 0.9.4 +version: 0.9.5 homepage: https://flutterprobe.dev repository: https://github.com/AlphaWaveSystems/flutter-probe issue_tracker: https://github.com/AlphaWaveSystems/flutter-probe/issues @@ -22,7 +22,7 @@ environment: dependencies: analyzer: ">=8.0.0 <14.0.0" build: ">=3.0.0 <5.0.0" - flutter_probe_annotation: ^0.9.4 + flutter_probe_annotation: ^0.9.5 source_gen: ^4.0.0 dev_dependencies: diff --git a/vscode/package.json b/vscode/package.json index 7f38e9d..2698e25 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -2,7 +2,7 @@ "name": "flutterprobe", "displayName": "FlutterProbe", "description": "High-performance E2E testing for Flutter apps — ProbeScript language support, local & cloud device testing (BrowserStack, Sauce Labs, AWS Device Farm, Firebase Test Lab, LambdaTest), visual regression, test recording, and Studio integration", - "version": "0.9.4", + "version": "0.9.5", "publisher": "flutterprobe", "icon": "resources/probe-icon.png", "engines": { "vscode": "^1.85.0" }, diff --git a/website/src/content/docs/tools/mcp.md b/website/src/content/docs/tools/mcp.md index ab5ce3e..b2c078e 100644 --- a/website/src/content/docs/tools/mcp.md +++ b/website/src/content/docs/tools/mcp.md @@ -238,7 +238,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | probe-mcp Expected response: ```json -{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"probe-mcp","version":"0.9.4"}}} +{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"probe-mcp","version":"0.9.5"}}} ``` List all available tools: