From da0ca85fc9958d6bbac1e3e9740293478c75b6c2 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 22 Oct 2025 11:17:57 -0700 Subject: [PATCH 1/4] Add remote tester bridge for Flutter integration tests Introduces the flet_integration_test Dart package and RemoteWidgetTester for TCP-based remote control of Flutter integration tests. Updates FletTestApp in Python to support a new 'remote' test mode, allowing tests to be driven without launching a Flet app. Refactors test setup and teardown logic to support both local and remote modes, and exposes RemoteTester in the Python SDK. --- client/integration_test/app_test.dart | 26 +- client/pubspec.lock | 7 + client/pubspec.yaml | 2 + packages/flet_integration_test/.gitignore | 32 +++ .../lib/flet_integration_test.dart | 3 + .../lib/src}/flutter_test_finder.dart | 2 +- .../lib/src}/flutter_tester.dart | 14 +- .../lib/src/remote_widget_tester.dart | 242 +++++++++++++++++ packages/flet_integration_test/pubspec.yaml | 20 ++ .../flet/src/flet/testing/__init__.py | 5 +- .../flet/src/flet/testing/flet_test_app.py | 154 +++++++---- .../flet/src/flet/testing/remote_tester.py | 243 ++++++++++++++++++ 12 files changed, 693 insertions(+), 57 deletions(-) create mode 100644 packages/flet_integration_test/.gitignore create mode 100644 packages/flet_integration_test/lib/flet_integration_test.dart rename {client/integration_test => packages/flet_integration_test/lib/src}/flutter_test_finder.dart (85%) rename {client/integration_test => packages/flet_integration_test/lib/src}/flutter_tester.dart (88%) create mode 100644 packages/flet_integration_test/lib/src/remote_widget_tester.dart create mode 100644 packages/flet_integration_test/pubspec.yaml create mode 100644 sdk/python/packages/flet/src/flet/testing/remote_tester.py diff --git a/client/integration_test/app_test.dart b/client/integration_test/app_test.dart index 9659913c55..fd5495212c 100644 --- a/client/integration_test/app_test.dart +++ b/client/integration_test/app_test.dart @@ -1,12 +1,11 @@ import 'dart:io'; import 'package:flet_client/main.dart' as app; +import 'package:flet_integration_test/flet_integration_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'flutter_tester.dart'; - void main() { var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -15,7 +14,20 @@ void main() { var dir = Directory.current.path; debugPrint("Current dir: $dir"); - app.tester = FlutterWidgetTester(tester, binding); + const testerServerUrl = String.fromEnvironment("FLET_TEST_SERVER_URL"); + FlutterWidgetTester? widgetTester; + + if (testerServerUrl.isNotEmpty) { + debugPrint("Connecting to remote tester at $testerServerUrl"); + widgetTester = await RemoteWidgetTester.connect( + tester: tester, + binding: binding, + serverUri: Uri.parse(testerServerUrl), + ); + } else { + widgetTester = FlutterWidgetTester(tester, binding); + app.tester = widgetTester; + } List args = []; const fletTestAppUrl = String.fromEnvironment("FLET_TEST_APP_URL"); @@ -36,10 +48,10 @@ void main() { app.main(args); await Future.delayed(const Duration(milliseconds: 500)); - await app.tester?.pump(duration: const Duration(seconds: 1)); - await app.tester - ?.pumpAndSettle(duration: const Duration(milliseconds: 100)); - await app.tester?.waitForTeardown(); + await widgetTester?.pump(duration: const Duration(seconds: 1)); + await widgetTester?.pumpAndSettle( + duration: const Duration(milliseconds: 100)); + await widgetTester?.waitForTeardown(); }); }); } diff --git a/client/pubspec.lock b/client/pubspec.lock index 6f8197566b..817d217973 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -313,6 +313,13 @@ packages: relative: true source: path version: "0.1.0" + flet_integration_test: + dependency: "direct dev" + description: + path: "../packages/flet_integration_test" + relative: true + source: path + version: "0.1.0" flet_lottie: dependency: "direct main" description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index ec66619c2c..1ce98838e7 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -86,6 +86,8 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter + flet_integration_test: + path: ../packages/flet_integration_test # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/packages/flet_integration_test/.gitignore b/packages/flet_integration_test/.gitignore new file mode 100644 index 0000000000..35ee281d14 --- /dev/null +++ b/packages/flet_integration_test/.gitignore @@ -0,0 +1,32 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ +.flutter-plugins +.flutter-plugins-dependencies diff --git a/packages/flet_integration_test/lib/flet_integration_test.dart b/packages/flet_integration_test/lib/flet_integration_test.dart new file mode 100644 index 0000000000..bfcdcaa9a0 --- /dev/null +++ b/packages/flet_integration_test/lib/flet_integration_test.dart @@ -0,0 +1,3 @@ +export 'src/flutter_test_finder.dart'; +export 'src/flutter_tester.dart'; +export 'src/remote_widget_tester.dart'; diff --git a/client/integration_test/flutter_test_finder.dart b/packages/flet_integration_test/lib/src/flutter_test_finder.dart similarity index 85% rename from client/integration_test/flutter_test_finder.dart rename to packages/flet_integration_test/lib/src/flutter_test_finder.dart index 645852a63f..742249f959 100644 --- a/client/integration_test/flutter_test_finder.dart +++ b/packages/flet_integration_test/lib/src/flutter_test_finder.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; class FlutterTestFinder extends TestFinder { final Finder finder; - FlutterTestFinder(this.finder) : super(); + FlutterTestFinder(this.finder); @override int get count => finder.evaluate().length; diff --git a/client/integration_test/flutter_tester.dart b/packages/flet_integration_test/lib/src/flutter_tester.dart similarity index 88% rename from client/integration_test/flutter_tester.dart rename to packages/flet_integration_test/lib/src/flutter_tester.dart index 47b368b4f7..c05e71828e 100644 --- a/client/integration_test/flutter_tester.dart +++ b/packages/flet_integration_test/lib/src/flutter_tester.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flet/flet.dart'; @@ -13,16 +14,20 @@ class FlutterWidgetTester implements Tester { final WidgetTester _tester; final IntegrationTestWidgetsFlutterBinding _binding; final lock = Lock(); - final Completer _teardown = Completer(); + final Completer _teardown = Completer(); FlutterWidgetTester(this._tester, this._binding); + @protected + IntegrationTestWidgetsFlutterBinding get binding => _binding; + @override Future pumpAndSettle({Duration? duration}) async { await lock.acquire(); try { - await _tester - .pumpAndSettle(duration ?? const Duration(milliseconds: 100)); + await _tester.pumpAndSettle( + duration ?? const Duration(milliseconds: 100), + ); } finally { lock.release(); } @@ -60,7 +65,8 @@ class FlutterWidgetTester implements Tester { if (defaultTargetPlatform != TargetPlatform.android && defaultTargetPlatform != TargetPlatform.iOS) { throw Exception( - "Full app screenshots are only available on Android and iOS."); + "Full app screenshots are only available on Android and iOS.", + ); } if (defaultTargetPlatform == TargetPlatform.android) { await _binding.convertFlutterSurfaceToImage(); diff --git a/packages/flet_integration_test/lib/src/remote_widget_tester.dart b/packages/flet_integration_test/lib/src/remote_widget_tester.dart new file mode 100644 index 0000000000..207741b8e0 --- /dev/null +++ b/packages/flet_integration_test/lib/src/remote_widget_tester.dart @@ -0,0 +1,242 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'flutter_test_finder.dart'; +import 'flutter_tester.dart'; + +class RemoteWidgetTester extends FlutterWidgetTester { + final Socket _socket; + final Map _finders = {}; + final Completer _connectionClosed = Completer(); + Future _commandQueue = Future.value(); + StreamSubscription? _subscription; + bool _teardownRequested = false; + + RemoteWidgetTester._( + WidgetTester tester, + IntegrationTestWidgetsFlutterBinding binding, + this._socket, + ) : super(tester, binding) { + _startListening(); + } + + static Future connect({ + required WidgetTester tester, + required IntegrationTestWidgetsFlutterBinding binding, + required Uri serverUri, + Duration timeout = const Duration(seconds: 10), + }) async { + if (!serverUri.hasPort) { + throw ArgumentError("Server URL must include a port: $serverUri"); + } + final host = serverUri.host.isEmpty ? "127.0.0.1" : serverUri.host; + final port = serverUri.port; + final socket = await Socket.connect(host, port, timeout: timeout); + socket.setOption(SocketOption.tcpNoDelay, true); + return RemoteWidgetTester._(tester, binding, socket); + } + + void _startListening() { + final lines = utf8.decoder.bind(_socket).transform(const LineSplitter()); + _subscription = lines.listen( + (line) { + _commandQueue = _commandQueue.then((_) => _processLine(line)); + }, + onError: (error, stackTrace) { + if (!_connectionClosed.isCompleted) { + _connectionClosed.completeError(error, stackTrace); + } + _closeSilently(); + }, + onDone: () { + _closeSilently(); + if (!_connectionClosed.isCompleted) { + _connectionClosed.complete(); + } + }, + cancelOnError: true, + ); + } + + Future _processLine(String line) async { + final dynamic decoded = jsonDecode(line); + if (decoded is! Map) { + throw Exception("Invalid command payload: $decoded"); + } + final id = decoded["id"]; + final method = decoded["method"] as String?; + final params = (decoded["params"] as Map?) + ?.cast() ?? + const {}; + + if (id == null || method == null) { + throw Exception("Command must include both 'id' and 'method'."); + } + + try { + final response = await _handleCommand(method, params); + _sendResponse(id, result: response.result); + if (response.closeAfter) { + await _socket.flush(); + await _socket.close(); + await _subscription?.cancel(); + if (!_connectionClosed.isCompleted) { + _connectionClosed.complete(); + } + } + } catch (error, stackTrace) { + _sendResponse(id, error: "$error", stack: stackTrace.toString()); + } + } + + _CommandResponse _ok([dynamic result]) => + _CommandResponse(result, closeAfter: false); + + Future<_CommandResponse> _handleCommand( + String method, + Map params, + ) async { + switch (method) { + case "pump": + await pump(duration: parseDuration(params["duration"])); + return _ok(); + case "pump_and_settle": + await pumpAndSettle(duration: parseDuration(params["duration"])); + return _ok(); + case "find_by_text": + return _ok(_storeFinder(findByText(params["text"] as String))); + case "find_by_text_containing": + return _ok( + _storeFinder(findByTextContaining(params["pattern"] as String)), + ); + case "find_by_key": + return _ok(_storeFinder(findByKey(_parseKey(params["key"])))); + case "find_by_tooltip": + return _ok(_storeFinder(findByTooltip(params["value"] as String))); + case "find_by_icon": + return _ok(_storeFinder(findByIcon(_parseIcon(params["icon"])))); + case "take_screenshot": + final bytes = await takeScreenshot(params["name"] as String); + return _ok(base64Encode(bytes)); + case "tap": + await _withFinder(params["id"], (finder) => tap(finder)); + return _ok(); + case "long_press": + await _withFinder(params["id"], (finder) => longPress(finder)); + return _ok(); + case "enter_text": + await _withFinder( + params["id"], + (finder) => enterText(finder, params["text"] as String), + ); + return _ok(); + case "mouse_hover": + await _withFinder(params["id"], (finder) => mouseHover(finder)); + return _ok(); + case "teardown": + _triggerTeardown(); + return const _CommandResponse(null, closeAfter: true); + default: + throw Exception("Unknown Tester method: $method"); + } + } + + Map _storeFinder(TestFinder finder) { + final flutterFinder = finder as FlutterTestFinder; + _finders[flutterFinder.id] = flutterFinder; + return flutterFinder.toMap(); + } + + Future _withFinder( + dynamic id, + Future Function(FlutterTestFinder finder) action, + ) async { + final finder = _finders[id]; + if (finder == null) { + throw Exception("Finder with id $id is not registered."); + } + await action(finder); + } + + IconData _parseIcon(dynamic value) { + if (value is Map) { + final codePoint = value["code_point"] as int?; + if (codePoint == null) { + throw Exception("Icon payload must include 'code_point'."); + } + return IconData( + codePoint, + fontFamily: value["font_family"] as String?, + fontPackage: value["font_package"] as String?, + matchTextDirection: (value["match_text_direction"] as bool?) ?? false, + ); + } else if (value is int) { + return IconData(value, fontFamily: "MaterialIcons"); + } + throw Exception("Invalid icon format: $value"); + } + + Key _parseKey(dynamic value) { + if (value is Map) { + final keyValue = value["value"]; + return ValueKey(keyValue); + } + return ValueKey(value); + } + + void _sendResponse( + dynamic id, { + dynamic result, + String? error, + String? stack, + }) { + final payload = {"id": id}; + if (error != null) { + payload["error"] = error; + if (stack != null) { + payload["stack"] = stack; + } + } else { + payload["result"] = result; + } + final encoded = jsonEncode(payload); + _socket.add(utf8.encode("$encoded\n")); + } + + void _closeSilently() { + _subscription?.cancel(); + _triggerTeardown(); + if (!_connectionClosed.isCompleted) { + _connectionClosed.complete(); + } + } + + void _triggerTeardown() { + if (_teardownRequested) { + return; + } + _teardownRequested = true; + super.teardown(); + } + + @override + void teardown() => _triggerTeardown(); + + @override + Future waitForTeardown() async { + await _commandQueue; + await Future.wait([super.waitForTeardown(), _connectionClosed.future]); + } +} + +class _CommandResponse { + final dynamic result; + final bool closeAfter; + + const _CommandResponse(this.result, {required this.closeAfter}); +} diff --git a/packages/flet_integration_test/pubspec.yaml b/packages/flet_integration_test/pubspec.yaml new file mode 100644 index 0000000000..2f396b4c26 --- /dev/null +++ b/packages/flet_integration_test/pubspec.yaml @@ -0,0 +1,20 @@ +name: flet_integration_test +description: Internal utilities for driving Flet Flutter integration tests. +publish_to: none +version: 0.1.0 + +environment: + sdk: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" + +dependencies: + flutter: + sdk: flutter + flet: + path: ../flet + integration_test: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/sdk/python/packages/flet/src/flet/testing/__init__.py b/sdk/python/packages/flet/src/flet/testing/__init__.py index fa6a212e4c..fb8733255a 100644 --- a/sdk/python/packages/flet/src/flet/testing/__init__.py +++ b/sdk/python/packages/flet/src/flet/testing/__init__.py @@ -1,5 +1,6 @@ from flet.testing.finder import Finder -from flet.testing.flet_test_app import FletTestApp +from flet.testing.flet_test_app import FletTestApp, FletTestMode +from flet.testing.remote_tester import RemoteTester from flet.testing.tester import Tester -__all__ = ["Finder", "FletTestApp", "Tester"] +__all__ = ["Finder", "FletTestApp", "FletTestMode", "RemoteTester", "Tester"] diff --git a/sdk/python/packages/flet/src/flet/testing/flet_test_app.py b/sdk/python/packages/flet/src/flet/testing/flet_test_app.py index 35b41612b8..b733a301b3 100644 --- a/sdk/python/packages/flet/src/flet/testing/flet_test_app.py +++ b/sdk/python/packages/flet/src/flet/testing/flet_test_app.py @@ -4,9 +4,10 @@ import platform import tempfile from collections.abc import Iterable +from enum import Enum from io import BytesIO from pathlib import Path -from typing import Any, Optional +from typing import Any, Optional, Union import numpy as np from PIL import Image @@ -14,11 +15,27 @@ import flet as ft from flet.controls.control import Control +from flet.testing.remote_tester import RemoteTester from flet.testing.tester import Tester from flet.utils.network import get_free_tcp_port from flet.utils.platform_utils import get_bool_env_var -__all__ = ["FletTestApp"] +__all__ = ["FletTestApp", "FletTestMode"] + + +class FletTestMode(Enum): + FLET = "flet" + REMOTE = "remote" + + @classmethod + def from_value(cls, value: Union["FletTestMode", str]) -> "FletTestMode": + if isinstance(value, cls): + return value + try: + return cls(value.lower()) + except ValueError as exc: + valid = ", ".join(mode.value for mode in cls) + raise ValueError(f"mode must be one of: {valid}") from exc class FletTestApp: @@ -80,6 +97,13 @@ class FletTestApp: If `True`, do not invoke `fvm` when running the Flutter test process. Env override: `FLET_TEST_DISABLE_FVM=1`. + mode: + Testing mode selector. `FletTestMode.FLET` (default) launches the + Python Flet app and communicates over the Flet protocol. + `FletTestMode.REMOTE` skips the Flet app and drives the Flutter + test harness through the remote tester bridge. Env override: + `FLET_TEST_MODE`. + Environment Variables: - `FLET_TEST_PLATFORM`: Overrides `test_platform`. - `FLET_TEST_DEVICE`: Overrides `test_device`. @@ -89,6 +113,8 @@ class FletTestApp: Overrides `screenshots_similarity_threshold`. - `FLET_TEST_USE_HTTP`: Enables HTTP transport when set to `1`. - `FLET_TEST_DISABLE_FVM`: Disables `fvm` usage when set to `1`. + - `FLET_TEST_MODE`: Overrides :param:`mode`. + - `FLET_TEST_SERVER_HOST`: Host interface to bind the remote tester server to. """ def __init__( @@ -105,6 +131,7 @@ def __init__( screenshots_similarity_threshold: float = 99.0, use_http: bool = False, disable_fvm: bool = False, + mode: FletTestMode = FletTestMode.FLET, ): self.test_platform = os.getenv("FLET_TEST_PLATFORM", test_platform) self.test_device = os.getenv("FLET_TEST_DEVICE", test_device) @@ -121,6 +148,10 @@ def __init__( ) ) self.__disable_fvm = get_bool_env_var("FLET_TEST_DISABLE_FVM") or disable_fvm + self.__mode = FletTestMode.from_value(mode) + env_mode = os.getenv("FLET_TEST_MODE") + if env_mode: + self.__mode = FletTestMode.from_value(env_mode) self.__use_http = get_bool_env_var("FLET_TEST_USE_HTTP") or use_http self.__test_path = test_path self.__flet_app_main = flet_app_main @@ -129,7 +160,8 @@ def __init__( self.__tcp_port = tcp_port self.__flutter_process: Optional[asyncio.subprocess.Process] = None self.__page = None - self.__tester = None + self.__tester: Optional[Union[Tester, RemoteTester]] = None + self.__remote_server_host = os.getenv("FLET_TEST_SERVER_HOST", "127.0.0.1") @property def page(self) -> ft.Page: @@ -137,14 +169,15 @@ def page(self) -> ft.Page: Returns an instance of Flet's app [`Page`][flet.]. """ if self.__page is None: - raise RuntimeError("page is not initialized") + raise RuntimeError( + "page is not initialized (available only when running in 'flet' mode)" + ) return self.__page @property - def tester(self) -> Tester: + def tester(self) -> Union[Tester, RemoteTester]: """ - Returns an instance of [`Tester`][flet.testing.] class - that programmatically interacts with page controls and the test environment. + Returns the active tester implementation. """ if self.__tester is None: raise RuntimeError("tester is not initialized") @@ -155,33 +188,49 @@ async def start(self): Starts Flet app and Flutter integration test process. """ - ready = asyncio.Event() + ready: Optional[asyncio.Event] = None + + if self.__mode is FletTestMode.FLET: + ready = asyncio.Event() - async def main(page: ft.Page): - self.__page = page - self.__tester = Tester() - page.theme_mode = ft.ThemeMode.LIGHT - page.update() + async def main(page: ft.Page): + self.__page = page + self.__tester = Tester() + page.theme_mode = ft.ThemeMode.LIGHT + page.update() - if asyncio.iscoroutinefunction(self.__flet_app_main): - await self.__flet_app_main(page) - elif callable(self.__flet_app_main): - self.__flet_app_main(page) - await self.__tester.pump_and_settle() - ready.set() + if asyncio.iscoroutinefunction(self.__flet_app_main): + await self.__flet_app_main(page) + elif callable(self.__flet_app_main): + self.__flet_app_main(page) + await self.__tester.pump_and_settle() + ready.set() - if not self.__tcp_port: - self.__tcp_port = get_free_tcp_port() + if not self.__tcp_port: + self.__tcp_port = get_free_tcp_port() - if self.__use_http: - os.environ["FLET_FORCE_WEB_SERVER"] = "true" + if self.__use_http: + os.environ["FLET_FORCE_WEB_SERVER"] = "true" - asyncio.create_task( - ft.run_async( - main, port=self.__tcp_port, assets_dir=str(self.__assets_dir), view=None + asyncio.create_task( + ft.run_async( + main, + port=self.__tcp_port, + assets_dir=str(self.__assets_dir), + view=None, + ) + ) + print("Started Flet app") + else: + remote_tester = RemoteTester() + self.__tcp_port = await remote_tester.start( + host=self.__remote_server_host, port=self.__tcp_port + ) + self.__tester = remote_tester + print( + "Started remote tester bridge " + f"on {self.__remote_server_host}:{self.__tcp_port}" ) - ) - print("Started Flet app") stdout = asyncio.subprocess.DEVNULL stderr = asyncio.subprocess.DEVNULL @@ -205,21 +254,25 @@ async def main(page: ft.Page): self.test_device = self.test_platform tcp_addr = "10.0.2.2" if self.test_platform == "android" else "127.0.0.1" - protocol = "http" if self.__use_http else "tcp" if self.test_device: flutter_args += ["-d", self.test_device] - app_url = f"{protocol}://{tcp_addr}:{self.__tcp_port}" - flutter_args += [f"--dart-define=FLET_TEST_APP_URL={app_url}"] - - if not self.__use_http: - temp_path = Path(tempfile.gettempdir()) / "flet_app_pid.txt" - flutter_args += [f"--dart-define=FLET_TEST_PID_FILE_PATH={temp_path}"] - if self.__assets_dir: - flutter_args += [ - f"--dart-define=FLET_TEST_ASSETS_DIR={self.__assets_dir}" - ] + if self.__mode is FletTestMode.FLET: + protocol = "http" if self.__use_http else "tcp" + app_url = f"{protocol}://{tcp_addr}:{self.__tcp_port}" + flutter_args += [f"--dart-define=FLET_TEST_APP_URL={app_url}"] + + if not self.__use_http: + temp_path = Path(tempfile.gettempdir()) / "flet_app_pid.txt" + flutter_args += [f"--dart-define=FLET_TEST_PID_FILE_PATH={temp_path}"] + if self.__assets_dir: + flutter_args += [ + f"--dart-define=FLET_TEST_ASSETS_DIR={self.__assets_dir}" + ] + else: + server_url = f"tcp://{tcp_addr}:{self.__tcp_port}" + flutter_args += [f"--dart-define=FLET_TEST_SERVER_URL={server_url}"] self.__flutter_process = await asyncio.create_subprocess_exec( *flutter_args, @@ -229,9 +282,20 @@ async def main(page: ft.Page): ) print("Started Flutter test process.") - print("Waiting for a Flet client to connect...") - - while not ready.is_set(): + if self.__mode is FletTestMode.FLET: + print("Waiting for a Flet client to connect...") + else: + print("Waiting for Flutter app to connect to remote tester...") + + while True: + if self.__mode is FletTestMode.FLET: + assert ready is not None + if ready.is_set(): + break + else: + assert isinstance(self.__tester, RemoteTester) + if self.__tester.is_connected(): + break await asyncio.sleep(0.2) if self.__flutter_process.returncode is not None: raise RuntimeError( @@ -244,7 +308,11 @@ async def teardown(self): Teardown Flutter integration test process. """ - await self.tester.teardown() + try: + await self.tester.teardown() + finally: + if isinstance(self.__tester, RemoteTester): + await self.__tester.stop() if self.__flutter_process: print("\nWaiting for Flutter test process to exit...") diff --git a/sdk/python/packages/flet/src/flet/testing/remote_tester.py b/sdk/python/packages/flet/src/flet/testing/remote_tester.py new file mode 100644 index 0000000000..ba9e33aa21 --- /dev/null +++ b/sdk/python/packages/flet/src/flet/testing/remote_tester.py @@ -0,0 +1,243 @@ +import asyncio +import base64 +import json +from dataclasses import asdict, is_dataclass +from typing import Any, Optional + +from flet.controls.duration import DurationValue +from flet.controls.keys import KeyValue +from flet.controls.types import IconData +from flet.testing.finder import Finder + +__all__ = ["RemoteTester", "RemoteTesterError"] + + +class RemoteTesterError(RuntimeError): + """Error returned from a remote tester invocation.""" + + def __init__(self, message: str, stack: Optional[str] = None): + super().__init__(message) + self.stack = stack + + +class RemoteTester: + """ + TCP-based tester implementation that talks directly to + the Flutter integration test harness. + """ + + def __init__(self): + self._server: Optional[asyncio.AbstractServer] = None + self._reader: Optional[asyncio.StreamReader] = None + self._writer: Optional[asyncio.StreamWriter] = None + self._connected = asyncio.Event() + self._closed = asyncio.Event() + self._closed.set() + self._pending: dict[int, asyncio.Future[Any]] = {} + self._request_id = 0 + self._reader_task: Optional[asyncio.Task[Any]] = None + self._send_lock = asyncio.Lock() + self.host = "127.0.0.1" + self.port: Optional[int] = None + + async def start(self, host: str = "127.0.0.1", port: Optional[int] = None) -> int: + """ + Starts the TCP server accepting connections from Flutter. + + Args: + host: Host interface to bind to. Defaults to ``127.0.0.1``. + port: Optional port to bind to. A random free port is used when + not specified. + + Returns: + Bound TCP port. + """ + if self._server is not None: + raise RuntimeError("RemoteTester server is already running.") + + self.host = host + self._server = await asyncio.start_server(self._handle_client, host, port) + sock = self._server.sockets[0] + self.port = sock.getsockname()[1] + return self.port + + async def _handle_client( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ): + if self._reader is not None: + writer.close() + await writer.wait_closed() + return + + self._reader = reader + self._writer = writer + self._closed.clear() + self._connected.set() + self._reader_task = asyncio.create_task(self._read_loop()) + + try: + await self._reader_task + finally: + self._cleanup_connection() + + async def _read_loop(self): + assert self._reader is not None + while True: + line = await self._reader.readline() + if not line: + break + message = json.loads(line.decode("utf-8")) + request_id = message.get("id") + future = self._pending.pop(request_id, None) + if future is None: + continue + if "error" in message: + future.set_exception( + RemoteTesterError(message["error"], message.get("stack")) + ) + else: + future.set_result(message.get("result")) + + def _cleanup_connection(self): + for future in self._pending.values(): + if not future.done(): + future.set_exception( + ConnectionError("Remote tester connection was closed.") + ) + self._pending.clear() + self._reader = None + self._writer = None + self._reader_task = None + self._connected.clear() + self._closed.set() + + async def stop(self): + """ + Stops the TCP server and releases any bound resources. + """ + if self._server is not None: + self._server.close() + await self._server.wait_closed() + self._server = None + + async def _ensure_connected(self): + await self._connected.wait() + if self._writer is None: + raise ConnectionError("Remote tester is not connected.") + + async def _invoke(self, method: str, params: Optional[dict[str, Any]] = None): + await self._ensure_connected() + self._request_id += 1 + request_id = self._request_id + + loop = asyncio.get_running_loop() + future: asyncio.Future[Any] = loop.create_future() + self._pending[request_id] = future + + payload = {"id": request_id, "method": method} + if params: + payload["params"] = params + + async with self._send_lock: + assert self._writer is not None + self._writer.write(json.dumps(payload).encode("utf-8") + b"\n") + await self._writer.drain() + + return await future + + async def pump(self, duration: Optional[DurationValue] = None): + await self._invoke( + "pump", _with_optional("duration", _serialize_duration(duration)) + ) + + async def pump_and_settle(self, duration: Optional[DurationValue] = None): + await self._invoke( + "pump_and_settle", + _with_optional("duration", _serialize_duration(duration)), + ) + + async def find_by_text(self, text: str) -> Finder: + finder = await self._invoke("find_by_text", {"text": text}) + return Finder(**finder) + + async def find_by_text_containing(self, pattern: str) -> Finder: + finder = await self._invoke("find_by_text_containing", {"pattern": pattern}) + return Finder(**finder) + + async def find_by_key(self, key: KeyValue) -> Finder: + finder = await self._invoke("find_by_key", {"key": _serialize_key(key)}) + return Finder(**finder) + + async def find_by_tooltip(self, value: str) -> Finder: + finder = await self._invoke("find_by_tooltip", {"value": value}) + return Finder(**finder) + + async def find_by_icon(self, icon: IconData) -> Finder: + finder = await self._invoke("find_by_icon", {"icon": _serialize_icon(icon)}) + return Finder(**finder) + + async def take_screenshot(self, name: str) -> bytes: + data = await self._invoke("take_screenshot", {"name": name}) + return base64.b64decode(data) + + async def tap(self, finder: Finder): + await self._invoke("tap", {"id": finder.id}) + + async def long_press(self, finder: Finder): + await self._invoke("long_press", {"id": finder.id}) + + async def enter_text(self, finder: Finder, text: str): + await self._invoke("enter_text", {"id": finder.id, "text": text}) + + async def mouse_hover(self, finder: Finder): + await self._invoke("mouse_hover", {"id": finder.id}) + + async def teardown(self): + if not self.is_connected(): + return + try: + await self._invoke("teardown") + finally: + await self.wait_closed() + + async def wait_closed(self): + await self._closed.wait() + + def is_connected(self) -> bool: + return self._connected.is_set() + + async def wait_for_connection(self, timeout: Optional[float] = None): + if timeout is not None: + await asyncio.wait_for(self._connected.wait(), timeout) + else: + await self._connected.wait() + + +def _with_optional(key: str, value: Any) -> dict[str, Any]: + return {key: value} if value is not None else {} + + +def _serialize_duration(duration: Optional[DurationValue]) -> Any: + if duration is None: + return None + if is_dataclass(duration): + return asdict(duration) + if isinstance(duration, (int, float)): + return int(duration) + return duration + + +def _serialize_key(value: KeyValue) -> Any: + if value is None: + return None + if is_dataclass(value): + return asdict(value) + return value + + +def _serialize_icon(icon: IconData) -> Any: + if icon is None: + return None + if hasattr(icon, "value"): + return int(icon) + return icon From 5c36b0a4ff6d622e404ac7de2b1e4084d08aa5fc Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 22 Oct 2025 11:59:29 -0700 Subject: [PATCH 2/4] Improve remote tester line decoding and iOS min version Introduces a custom ChunkedLineDecoder for robust UTF-8 line splitting with max length enforcement in Dart and Python remote tester implementations. Updates iOS minimum deployment target to 13.0 in project files. Refactors remote tester logic to handle large messages and prevent buffer overflows. --- client/integration_test/app_test.dart | 11 +-- client/ios/Flutter/AppFrameworkInfo.plist | 2 +- client/ios/Podfile | 2 +- client/ios/Podfile.lock | 10 +-- client/ios/Runner.xcodeproj/project.pbxproj | 6 +- .../lib/src/chunked_line_decoder.dart | 72 +++++++++++++++++++ .../lib/src/remote_widget_tester.dart | 39 +++++----- .../flet/src/flet/testing/remote_tester.py | 43 +++++++---- 8 files changed, 139 insertions(+), 46 deletions(-) create mode 100644 packages/flet_integration_test/lib/src/chunked_line_decoder.dart diff --git a/client/integration_test/app_test.dart b/client/integration_test/app_test.dart index fd5495212c..5c795b7711 100644 --- a/client/integration_test/app_test.dart +++ b/client/integration_test/app_test.dart @@ -47,10 +47,13 @@ void main() { app.main(args); - await Future.delayed(const Duration(milliseconds: 500)); - await widgetTester?.pump(duration: const Duration(seconds: 1)); - await widgetTester?.pumpAndSettle( - duration: const Duration(milliseconds: 100)); + if (testerServerUrl.isEmpty) { + await Future.delayed(const Duration(milliseconds: 500)); + await widgetTester?.pump(duration: const Duration(seconds: 1)); + await widgetTester?.pumpAndSettle( + duration: const Duration(milliseconds: 100), + ); + } await widgetTester?.waitForTeardown(); }); }); diff --git a/client/ios/Flutter/AppFrameworkInfo.plist b/client/ios/Flutter/AppFrameworkInfo.plist index 7c56964006..1dc6cf7652 100644 --- a/client/ios/Flutter/AppFrameworkInfo.plist +++ b/client/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/client/ios/Podfile b/client/ios/Podfile index 412ab380b1..1685edd78a 100644 --- a/client/ios/Podfile +++ b/client/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index 68492f8268..f69bacb9b5 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -62,7 +62,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - record_ios (1.0.0): + - record_ios (1.1.0): - Flutter - rive_common (0.0.1): - Flutter @@ -169,7 +169,7 @@ SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e Google-Mobile-Ads-SDK: 1dfb0c3cb46c7e2b00b0f4de74a1e06d9ea25d67 google_mobile_ads: 535223588a6791b7a3cc3513a1bc7b89d12f3e62 @@ -180,7 +180,7 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b + record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 rive_common: dd421daaf9ae69f0125aa761dd96abd278399952 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b @@ -190,8 +190,8 @@ SPEC CHECKSUMS: url_launcher_ios: 694010445543906933d732453a59da0a173ae33d volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 - webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d -PODFILE CHECKSUM: 6e0773c9c44c19ccfa69850451666ad1d1af99d1 +PODFILE CHECKSUM: 462a5b249f9f1900cbd87af7b6af48272dc2df5a COCOAPODS: 1.14.3 diff --git a/client/ios/Runner.xcodeproj/project.pbxproj b/client/ios/Runner.xcodeproj/project.pbxproj index 979c654f03..00bb254787 100644 --- a/client/ios/Runner.xcodeproj/project.pbxproj +++ b/client/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -441,7 +441,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -490,7 +490,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/flet_integration_test/lib/src/chunked_line_decoder.dart b/packages/flet_integration_test/lib/src/chunked_line_decoder.dart new file mode 100644 index 0000000000..d62b382f25 --- /dev/null +++ b/packages/flet_integration_test/lib/src/chunked_line_decoder.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +/// Stream transformer which decodes UTF-8 bytes into newline-delimited strings +/// while enforcing a maximum line length. +class ChunkedLineDecoder extends StreamTransformerBase { + final int maxLineLength; + + const ChunkedLineDecoder({this.maxLineLength = 16 * 1024 * 1024}); + + @override + Stream bind(Stream stream) { + final stringStream = stream.cast>().transform(utf8.decoder); + late StreamSubscription subscription; + late StreamController controller; + var pending = ""; + + void emitLine(String line) { + if (line.length > maxLineLength) { + throw StateError( + "Line length exceeds allowed limit of $maxLineLength characters.", + ); + } + controller.add(line); + } + + controller = StreamController( + onListen: () { + subscription = stringStream.listen( + (chunk) { + pending += chunk; + while (true) { + final newlineIndex = pending.indexOf('\n'); + if (newlineIndex == -1) { + if (pending.length > maxLineLength) { + controller.addError(StateError( + "Line length exceeds allowed limit of $maxLineLength characters.", + )); + subscription.cancel(); + } + break; + } + final line = pending.substring(0, newlineIndex); + emitLine(line); + pending = pending.substring(newlineIndex + 1); + } + }, + onError: controller.addError, + onDone: () { + if (pending.isNotEmpty) { + if (pending.length > maxLineLength) { + controller.addError(StateError( + "Line length exceeds allowed limit of $maxLineLength characters.", + )); + } else { + controller.add(pending); + } + } + controller.close(); + }, + cancelOnError: false, + ); + }, + onPause: () => subscription.pause(), + onResume: () => subscription.resume(), + onCancel: () => subscription.cancel(), + ); + + return controller.stream; + } +} diff --git a/packages/flet_integration_test/lib/src/remote_widget_tester.dart b/packages/flet_integration_test/lib/src/remote_widget_tester.dart index 207741b8e0..b6566748d9 100644 --- a/packages/flet_integration_test/lib/src/remote_widget_tester.dart +++ b/packages/flet_integration_test/lib/src/remote_widget_tester.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flet/flet.dart'; +import 'package:flet_integration_test/src/chunked_line_decoder.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -42,25 +43,25 @@ class RemoteWidgetTester extends FlutterWidgetTester { } void _startListening() { - final lines = utf8.decoder.bind(_socket).transform(const LineSplitter()); - _subscription = lines.listen( - (line) { - _commandQueue = _commandQueue.then((_) => _processLine(line)); - }, - onError: (error, stackTrace) { - if (!_connectionClosed.isCompleted) { - _connectionClosed.completeError(error, stackTrace); - } - _closeSilently(); - }, - onDone: () { - _closeSilently(); - if (!_connectionClosed.isCompleted) { - _connectionClosed.complete(); - } - }, - cancelOnError: true, - ); + final stream = _socket + .transform(const ChunkedLineDecoder(maxLineLength: 16 * 1024 * 1024)); + _subscription = stream.listen(null, onError: (error, stackTrace) { + if (!_connectionClosed.isCompleted) { + _connectionClosed.completeError(error, stackTrace); + } + _closeSilently(); + }, onDone: () { + _closeSilently(); + if (!_connectionClosed.isCompleted) { + _connectionClosed.complete(); + } + }, cancelOnError: true); + _subscription!.onData((line) { + _subscription?.pause(); + final Future pending = _processLine(line); + _commandQueue = _commandQueue.then((_) => pending); + pending.whenComplete(() => _subscription?.resume()); + }); } Future _processLine(String line) async { diff --git a/sdk/python/packages/flet/src/flet/testing/remote_tester.py b/sdk/python/packages/flet/src/flet/testing/remote_tester.py index ba9e33aa21..4f56ad1891 100644 --- a/sdk/python/packages/flet/src/flet/testing/remote_tester.py +++ b/sdk/python/packages/flet/src/flet/testing/remote_tester.py @@ -82,21 +82,38 @@ async def _handle_client( async def _read_loop(self): assert self._reader is not None + buffer = bytearray() + max_line_length = 16 * 1024 * 1024 while True: - line = await self._reader.readline() - if not line: + chunk = await self._reader.read(4096) + if not chunk: + if buffer: + message = json.loads(buffer.decode("utf-8")) + self._dispatch_message(message) break - message = json.loads(line.decode("utf-8")) - request_id = message.get("id") - future = self._pending.pop(request_id, None) - if future is None: - continue - if "error" in message: - future.set_exception( - RemoteTesterError(message["error"], message.get("stack")) - ) - else: - future.set_result(message.get("result")) + buffer.extend(chunk) + while True: + newline_index = buffer.find(b"\n") + if newline_index == -1: + if len(buffer) > max_line_length: + raise ValueError("Incoming message exceeds allowed size.") + break + line = buffer[:newline_index] + del buffer[: newline_index + 1] + message = json.loads(line.decode("utf-8")) + self._dispatch_message(message) + + def _dispatch_message(self, message: Any): + request_id = message.get("id") + future = self._pending.pop(request_id, None) + if future is None: + return + if "error" in message: + future.set_exception( + RemoteTesterError(message["error"], message.get("stack")) + ) + else: + future.set_result(message.get("result")) def _cleanup_connection(self): for future in self._pending.values(): From 4554fc3bbfbdc3eb5ab8c28160f887bb1f5ace13 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 22 Oct 2025 13:35:21 -0700 Subject: [PATCH 3/4] Refactor CLI argument parsing and subparser handling Improves type annotations and refactors the set_default_subparser function to return the modified argument list. Adds a run() function for better testability and separates parser creation logic. The main entry point now calls run() and handles exit codes more cleanly. --- .../packages/flet-cli/src/flet_cli/cli.py | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/cli.py b/sdk/python/packages/flet-cli/src/flet_cli/cli.py index 62b50ad924..54d2ce89c4 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/cli.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/cli.py @@ -1,5 +1,6 @@ import argparse import sys +from typing import Optional import flet.version import flet_cli.commands.build @@ -14,8 +15,11 @@ # Source https://stackoverflow.com/a/26379693 def set_default_subparser( - parser: argparse.ArgumentParser, name: str, args: list = None, index: int = 0 -): + parser: argparse.ArgumentParser, + name: str, + args: Optional[list[str]] = None, + index: int = 0, +) -> list[str]: """ Set a default subparser when no subparser is provided. This should be called after setting up the argument parser but before @@ -28,9 +32,12 @@ def set_default_subparser( inserted. """ + mutate_sys_argv = args is None + current_args = list(sys.argv[1:] if mutate_sys_argv else args) + # exit if help or version flags are present - if any(flag in sys.argv[1:] for flag in {"-h", "--help", "-V", "--version"}): - return + if any(flag in current_args for flag in {"-h", "--help", "-V", "--version"}): + return current_args # all subparser actions subparser_actions = [ @@ -45,22 +52,27 @@ def set_default_subparser( ] # if an existing subparser is provided, skip setting a default - if any(arg in subparser_names for arg in sys.argv[1:]): - return + if any(arg in subparser_names for arg in current_args): + return current_args # if the default subparser doesn't exist, register it in the first subparser action if (name not in subparser_names) and subparser_actions: subparser_actions[0].add_parser(name) # insert the default subparser into the appropriate argument list - if args is None: - if len(sys.argv) > 1: - sys.argv.insert(index, name) - else: - args.insert(index, name) + current_args.insert(index, name) + + if mutate_sys_argv: + sys.argv = [sys.argv[0], *current_args] + + return current_args def main(): + sys.exit(run()) + + +def _create_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument( "--version", @@ -81,19 +93,29 @@ def main(): sp, "doctor" ) # Register the doctor command + return parser + + +def run(args: Optional[list[str]] = None) -> int: + parser = _create_parser() + + argv = list(args) if args is not None else list(sys.argv[1:]) + # set "run" as the default subparser - set_default_subparser(parser, name="run", index=1) + argv = set_default_subparser(parser, name="run", args=argv, index=0) # print usage/help if called without arguments - if len(sys.argv) == 1: + if not argv: parser.print_help(sys.stdout) - sys.exit(1) + return 1 # parse arguments - args = parser.parse_args() + namespace = parser.parse_args(argv) # execute command - args.handler(args) + namespace.handler(namespace) + + return 0 if __name__ == "__main__": From e06f6e08ce624875a8535e1fb7bb10182aafbc4b Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 27 Nov 2025 17:32:56 -0800 Subject: [PATCH 4/4] Update finder parameter handling in widget tester Replaces usage of 'id' with 'finder_id' and adds support for 'finder_index' in tap, long_press, enter_text, and mouse_hover actions in RemoteWidgetTester. Also removes unused import from flutter_tester.dart. --- .../lib/src/flutter_tester.dart | 1 - .../lib/src/remote_widget_tester.dart | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/flet_integration_test/lib/src/flutter_tester.dart b/packages/flet_integration_test/lib/src/flutter_tester.dart index 7539c39c24..ffef91a748 100644 --- a/packages/flet_integration_test/lib/src/flutter_tester.dart +++ b/packages/flet_integration_test/lib/src/flutter_tester.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'dart:ui'; import 'package:flet/flet.dart'; diff --git a/packages/flet_integration_test/lib/src/remote_widget_tester.dart b/packages/flet_integration_test/lib/src/remote_widget_tester.dart index b6566748d9..767a1c3d2b 100644 --- a/packages/flet_integration_test/lib/src/remote_widget_tester.dart +++ b/packages/flet_integration_test/lib/src/remote_widget_tester.dart @@ -125,19 +125,23 @@ class RemoteWidgetTester extends FlutterWidgetTester { final bytes = await takeScreenshot(params["name"] as String); return _ok(base64Encode(bytes)); case "tap": - await _withFinder(params["id"], (finder) => tap(finder)); + await _withFinder(params["finder_id"], + (finder) => tap(finder, params["finder_index"])); return _ok(); case "long_press": - await _withFinder(params["id"], (finder) => longPress(finder)); + await _withFinder(params["finder_id"], + (finder) => longPress(finder, params["finder_index"])); return _ok(); case "enter_text": await _withFinder( - params["id"], - (finder) => enterText(finder, params["text"] as String), + params["finder_id"], + (finder) => enterText( + finder, params["finder_index"], params["text"] as String), ); return _ok(); case "mouse_hover": - await _withFinder(params["id"], (finder) => mouseHover(finder)); + await _withFinder(params["finder_id"], + (finder) => mouseHover(finder, params["finder_index"])); return _ok(); case "teardown": _triggerTeardown();