From b1390315bfb8029c4742457f91efc4f5cb0b77f4 Mon Sep 17 00:00:00 2001 From: tan Date: Mon, 2 Dec 2024 09:21:36 +0700 Subject: [PATCH] update extension command files and readme --- README.md | 37 ++++-- example/dart/drag_commands.dart | 68 +++++++++++ example/dart/get_text_command.dart | 180 +++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 example/dart/drag_commands.dart create mode 100644 example/dart/get_text_command.dart diff --git a/README.md b/README.md index 84464650..9c522087 100644 --- a/README.md +++ b/README.md @@ -360,17 +360,16 @@ Available commands: ### How to use 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: [extended_commands.dart](extended_commands.dart) -- getTextWithCommandExtension: [get_text_command.dart](get_text_command.dart) +- 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(), GetTextCommandExtension()]); @@ -378,10 +377,10 @@ void main() { } ``` -#### 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"] @@ -397,16 +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 example using `getTextWithCommandExtension` command in nodejs +#### 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 result = await driver.execute('flutter:getTextWithCommandExtension', payload); +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/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!, + }; + } +}