diff --git a/README.md b/README.md index 1ddba7b2..9c522087 100644 --- a/README.md +++ b/README.md @@ -355,29 +355,32 @@ This is a command extension for Flutter Driver, utilizing the [CommandExtension- Available commands: - `dragAndDropWithCommandExtension` – performs a drag-and-drop action on the screen by specifying the start and end coordinates and the action duration. +- `getTextWithCommandExtension` - get text data from Text widget that contains TextSpan widgets. ### How to use -Copy the [extended_commands.dart](extended_commands.dart) file to the `lib` folder of your Flutter project. +Copy the sample dart files to the `lib` folder of your project. Please note that you don't need to copy all files, just copy the file matched with the command you need. +- dragAndDropWithCommandExtension: [drag_commands.dart](./example/dart/drag_commands.dart) +- getTextWithCommandExtension: [get_text_command.dart](./example/dart/get_text_command.dart) The entry point must include the `List?` commands argument in either `main.dart` or `test_main.dart` to properly handle the command extension. ```dart -import 'extended_commands.dart'; - +import 'drag_commands.dart'; +import 'get_text_command.dart'; void main() { enableFlutterDriverExtension( - commands: [DragCommandExtension()]); + commands: [DragCommandExtension(), GetTextCommandExtension()]); runApp(const MyApp()); } ``` -#### Simple example using `dragAndDropWithCommandExtension` command in Python +#### Simple examples in Python ```python -# python +# Extended commands: flutter:dragAndDropWithCommandExtension coord_item_1 = driver.execute_script("flutter:getCenter", item_1) coord_item_2 = driver.execute_script("flutter:getCenter", item_2) start_x = coord_item_1["dx"] @@ -393,6 +396,38 @@ payload = { } driver.execute_script("flutter:dragAndDropWithCommandExtension", payload) + +# Extended commands: flutter:getTextWithCommandExtension +text_finder = finder.by_value_key('amount') +get_text_payload = { + 'findBy': text_finder, +} +result = driver.execute_script('flutter:getTextWithCommandExtension', payload) +print(result) +``` + +#### Simple examples in nodejs + +```typescript +// Extended commands: flutter:dragAndDropWithCommandExtension +const payload = { + "startX": "100", + "startY": "100", + "endX": "100", + "endY": "600", + "duration": "15000" +} +const result = await driver.execute("flutter:dragAndDropWithCommandExtension", payload); +console.log(JSON.stringify(result)); + +// Extended commands: flutter:getTextWithCommandExtension +import {byValueKey} from "appium-flutter-finder"; +const payload = { + 'findBy': byValueKey('amount'), + }; +const getTextResult = await driver.execute('flutter:getTextWithCommandExtension', payload); +console.log(JSON.stringify(getTextResult)); + ``` For debugging or testing in other programming languages, you can use the APK available in this [repository](https://github.com/Alpaca00/command-driven-list) or build an IPA. diff --git a/driver/lib/commands/execute.ts b/driver/lib/commands/execute.ts index 6e2cfbfa..00b7d227 100644 --- a/driver/lib/commands/execute.ts +++ b/driver/lib/commands/execute.ts @@ -86,6 +86,8 @@ export const execute = async function( return await clickElement(this, args[0], args[1]); case `dragAndDropWithCommandExtension`: return await dragAndDropWithCommandExtension(this, args[0]); + case `getTextWithCommandExtension`: + return await getTextWithCommandExtension(this, args[0]); default: throw new Error(`Command not support: "${rawCommand}"`); } @@ -242,3 +244,11 @@ const dragAndDropWithCommandExtension = async ( }; return await self.socket!.executeSocketCommand(commandPayload); }; + +const getTextWithCommandExtension = async (self: FlutterDriver, params: { findBy: string; }) => { + const payload = { + command: 'getTextWithCommandExtension', + findBy: params.findBy, + }; + return await self.socket!.executeSocketCommand(payload); +}; diff --git a/example/dart/drag_commands.dart b/example/dart/drag_commands.dart new file mode 100644 index 00000000..c38aa524 --- /dev/null +++ b/example/dart/drag_commands.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_driver/src/common/message.dart'; +import 'package:flutter_driver/src/extension/extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + + +class DragCommand extends Command { + final double startX; + final double startY; + final double endX; + final double endY; + final Duration duration; + + DragCommand(this.startX, this.startY, this.endX, this.endY, this.duration); + + @override + String get kind => 'dragAndDropWithCommandExtension'; + + DragCommand.deserialize(Map params) + : startX = double.parse(params['startX']!), + startY = double.parse(params['startY']!), + endX = double.parse(params['endX']!), + endY = double.parse(params['endY']!), + duration = Duration(milliseconds: int.parse(params['duration']!)); +} + + +class DragResult extends Result { + final bool success; + + const DragResult(this.success); + + @override + Map toJson() { + return { + 'success': success, + }; + } +} + + +class DragCommandExtension extends CommandExtension { + @override + Future call(Command command, WidgetController prober, + CreateFinderFactory finderFactory, CommandHandlerFactory handlerFactory) async { + final DragCommand dragCommand = command as DragCommand; + + final Offset startLocation = Offset(dragCommand.startX, dragCommand.startY); + final Offset offset = Offset(dragCommand.endX - dragCommand.startX, dragCommand.endY - dragCommand.startY); + + await prober.timedDragFrom(startLocation, offset, dragCommand.duration); + + return const DragResult(true); + } + + @override + String get commandKind => 'dragAndDropWithCommandExtension'; + + @override + Command deserialize( + Map params, + DeserializeFinderFactory finderFactory, + DeserializeCommandFactory commandFactory) { + return DragCommand.deserialize(params); + } +} diff --git a/example/dart/get_text_command.dart b/example/dart/get_text_command.dart new file mode 100644 index 00000000..b8523351 --- /dev/null +++ b/example/dart/get_text_command.dart @@ -0,0 +1,180 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_driver/src/common/find.dart'; +import 'package:flutter_driver/src/common/message.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class Base64URL { + static String encode(String str) { + String base64 = base64Encode(utf8.encode(str)); + return base64.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); + } + + static String decode(String str) { + String base64 = str.replaceAll('-', '+').replaceAll('_', '/'); + + // Add padding if needed + switch (base64.length % 4) { + case 2: + base64 += '=='; + break; + case 3: + base64 += '='; + break; + } + + return utf8.decode(base64Decode(base64)); + } +} + +class FinderHelper { + static SerializableFinder deserializeBase64(String base64Str) { + try { + // Decode base64 to JSON string + final jsonStr = Base64URL.decode(base64Str); + + // Parse JSON + final dynamic finderData = json.decode(jsonStr); + + if (finderData is! Map) { + throw Exception('finder is not valid'); + } + + if (!finderData.containsKey('finderType')) { + throw Exception('Invalid finder format: missing finderType'); + } + + final String finderType = finderData['finderType'] as String; + + switch (finderType) { + case 'ByText': + return ByText(finderData['text'] as String); + + case 'ByType': + return ByType(finderData['type'] as String); + + case 'ByValueKey': + final keyType = finderData['keyValueType'] as String?; + final keyValue = finderData['keyValueString'] as String; + + if (keyType == 'int') { + return ByValueKey(int.parse(keyValue)); + } + return ByValueKey(keyValue); + + case 'Ancestor': + // Parse of and matching which are JSON strings + final ofJson = json.decode(finderData['of'] as String); + final matchingJson = json.decode(finderData['matching'] as String); + + return Ancestor( + of: deserializeBase64(Base64URL.encode(json.encode(ofJson))), + matching: + deserializeBase64(Base64URL.encode(json.encode(matchingJson))), + matchRoot: finderData['matchRoot'] == 'true', + firstMatchOnly: finderData['firstMatchOnly'] == 'true', + ); + + case 'Descendant': + final ofJson = json.decode(finderData['of'] as String); + final matchingJson = json.decode(finderData['matching'] as String); + + return Descendant( + of: deserializeBase64(Base64URL.encode(json.encode(ofJson))), + matching: + deserializeBase64(Base64URL.encode(json.encode(matchingJson))), + matchRoot: finderData['matchRoot'] == 'true', + firstMatchOnly: finderData['firstMatchOnly'] == 'true', + ); + + default: + throw Exception('Unsupported finder type: $finderType'); + } + } catch (e) { + throw Exception('Error deserializing finder: $e'); + } + } +} + +class GetTextCommandExtension extends CommandExtension { + String? getTextFromWidget(Text widget) { + return widget.data ?? widget.textSpan?.toPlainText(); + } + + @override + Future call( + Command command, + WidgetController prober, + CreateFinderFactory finderFactory, + CommandHandlerFactory handlerFactory) async { + final GetTextCommand dragCommand = command as GetTextCommand; + + // Create finder for Text widget + final type = dragCommand.base64Element; + // decodeBase64 to json + SerializableFinder serializableFinder = + FinderHelper.deserializeBase64(type); + + final Finder finder = finderFactory.createFinder(serializableFinder); + + // Get the widget element + final Element element = prober.element(finder); + + // if element is not a Text widget, return false with error + if (element.widget is! Text) { + return const GetTextResult(false, data: { + 'errorCode': 'NOT_A_TEXT_WIDGET', + 'error': 'Found element is not a Text widget' + }); + } + + final text = getTextFromWidget(element.widget as Text); + return text != null + ? GetTextResult(true, data: {'text': text}) + : const GetTextResult(false, data: { + 'errorCode': 'NO_TEXT_CONTENT', + 'error': 'No text content found' + }); + } + + @override + String get commandKind => 'getTextWithCommandExtension'; + + @override + Command deserialize( + Map params, + DeserializeFinderFactory finderFactory, + DeserializeCommandFactory commandFactory) { + return GetTextCommand.deserialize(params); + } +} + +class GetTextCommand extends Command { + final String base64Element; + + GetTextCommand(this.base64Element); + + @override + String get kind => 'getTextWithCommandExtension'; + + GetTextCommand.deserialize(Map params) + : base64Element = params['findBy']!; +} + +class GetTextResult extends Result { + final bool success; + final Map? data; + + const GetTextResult(this.success, {this.data}); + + @override + Map toJson() { + return { + 'success': success, + if (data != null) ...data!, + }; + } +}