Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions probe_agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
66 changes: 54 additions & 12 deletions probe_agent/lib/src/executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -648,26 +648,68 @@ class ProbeExecutor {
// ---- Screenshot ----

Future<String> _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<Uint8List?> _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<Uint8List?> _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<void> _openLink(String url) async {
Expand Down
2 changes: 1 addition & 1 deletion probe_agent/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions probe_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion probe_annotation/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions probe_gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions probe_gen/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/tools/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading