diff --git a/lib/extended_commands.dart b/lib/drag_commands.dart similarity index 100% rename from lib/extended_commands.dart rename to lib/drag_commands.dart diff --git a/lib/get_text_command.dart b/lib/get_text_command.dart new file mode 100644 index 0000000..b852335 --- /dev/null +++ b/lib/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!, + }; + } +} diff --git a/lib/main.dart b/lib/main.dart index 0f25b4d..6b5cfc3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/src/extension/extension.dart'; -import 'extended_commands.dart'; - +import 'drag_commands.dart'; +import 'get_text_command.dart'; void main() { - enableFlutterDriverExtension( - commands: [DragCommandExtension()]); + enableFlutterDriverExtension(commands: [DragCommandExtension(), GetTextCommandExtension()]); runApp(const MyApp()); } @@ -29,6 +28,7 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); + final String title; @override @@ -45,22 +45,62 @@ class _MyHomePageState extends State { title: Text(widget.title), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), - body: ReorderableListView( - padding: const EdgeInsets.all(8), - children: items.map((item) { - return ListTile( - key: ValueKey(item), - title: Text(item), - tileColor: Colors.grey.shade200, - ); - }).toList(), - onReorder: (oldIndex, newIndex) { - setState(() { - if (newIndex > oldIndex) newIndex -= 1; - final item = items.removeAt(oldIndex); - items.insert(newIndex, item); - }); - }, + body: Column( + children: [ + // Original ReorderableListView + Expanded( + child: ReorderableListView( + padding: const EdgeInsets.all(8), + children: items.map((item) { + return ListTile( + key: ValueKey(item), + title: Text(item), + tileColor: Colors.grey.shade200, + ); + }).toList(), + onReorder: (oldIndex, newIndex) { + setState(() { + if (newIndex > oldIndex) newIndex -= 1; + final item = items.removeAt(oldIndex); + items.insert(newIndex, item); + }); + }, + ), + ), + // TextSpan example + const Padding( + padding: EdgeInsets.all(16.0), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Amount: ', + style: TextStyle( + fontSize: 18, + color: Colors.grey, + ), + ), + TextSpan( + text: '100', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + TextSpan( + text: ' USD', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ], + ), + key: ValueKey('amount'), + ), + ), + ], ), ); } diff --git a/test_driver/app.dart b/test_driver/app.dart index b48f220..4e8d33c 100644 --- a/test_driver/app.dart +++ b/test_driver/app.dart @@ -1,4 +1,4 @@ -import 'package:demo/extended_commands.dart'; +import 'package:demo/drag_commands.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:demo/main.dart' as app;