From f219fa41bdc89137408326b7247064633299c66b Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 30 Nov 2024 23:07:50 +0700 Subject: [PATCH 1/4] feat: add getText enhancement for complex text widgets --- driver/lib/commands/execute.ts | 149 ++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 69 deletions(-) diff --git a/driver/lib/commands/execute.ts b/driver/lib/commands/execute.ts index 6e2cfbfa..0c6c26e7 100644 --- a/driver/lib/commands/execute.ts +++ b/driver/lib/commands/execute.ts @@ -20,75 +20,78 @@ export const execute = async function( throw new Error(`Command not support: "${rawCommand}"`); } - const command = matching[1].trim(); - switch (command) { - case `launchApp`: - return await flutterLaunchApp(this, args[0], args[1]); - case `connectObservatoryWsUrl`: - return await connectObservatoryWsUrl(this); - case `getVMInfo`: - return await getVMInfo(this); - case `setIsolateId`: - return await setIsolateId(this, args[0]); - case `getIsolate`: - return await getIsolate(this, args[0]); - case `checkHealth`: - return await checkHealth(this); - case `clearTimeline`: - return await clearTimeline(this); - case `forceGC`: - return await forceGC(this); - case `getRenderTree`: - return await getRenderTree(this); - case `getBottomLeft`: - return await getOffset(this, args[0], { offsetType: `bottomLeft` }); - case `getBottomRight`: - return await getOffset(this, args[0], { offsetType: `bottomRight` }); - case `getCenter`: - return await getOffset(this, args[0], { offsetType: `center` }); - case `getTopLeft`: - return await getOffset(this, args[0], { offsetType: `topLeft` }); - case `getTopRight`: - return await getOffset(this, args[0], { offsetType: `topRight` }); - case `getRenderObjectDiagnostics`: - return await getRenderObjectDiagnostics(this, args[0], args[1]); - case `getWidgetDiagnostics`: - return await getWidgetDiagnostics(this, args[0], args[1]); - case `getSemanticsId`: - return await getSemanticsId(this, args[0]); - case `waitForAbsent`: - return await waitForAbsent(this, args[0], args[1]); - case `waitFor`: - return await waitFor(this, args[0], args[1]); - case `waitForTappable`: - return await waitForTappable(this, args[0], args[1]); - case `scroll`: - return await scroll(this, args[0], args[1]); - case `scrollUntilVisible`: - return await scrollUntilVisible(this, args[0], args[1]); - case `scrollUntilTapable`: - return await scrollUntilTapable(this, args[0], args[1]); - case `scrollIntoView`: - return await scrollIntoView(this, args[0], args[1]); - case `setTextEntryEmulation`: - return await setTextEntryEmulation(this, args[0]); - case `enterText`: - return await enterText(this, args[0]); - case `requestData`: - return await requestData(this, args[0]); - case `longTap`: - return await longTap(this, args[0], args[1]); - case `waitForFirstFrame`: - return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`}); - case `setFrameSync`: - return await setFrameSync(this, args[0], args[1]); - case `clickElement`: - return await clickElement(this, args[0], args[1]); - case `dragAndDropWithCommandExtension`: - return await dragAndDropWithCommandExtension(this, args[0]); - default: - throw new Error(`Command not support: "${rawCommand}"`); - } + const command = matching[1].trim(); + + switch (command) { + case `launchApp`: + return await flutterLaunchApp(this, args[0], args[1]); + case `connectObservatoryWsUrl`: + return await connectObservatoryWsUrl(this); + case `getVMInfo`: + return await getVMInfo(this); + case `setIsolateId`: + return await setIsolateId(this, args[0]); + case `getIsolate`: + return await getIsolate(this, args[0]); + case `checkHealth`: + return await checkHealth(this); + case `clearTimeline`: + return await clearTimeline(this); + case `forceGC`: + return await forceGC(this); + case `getRenderTree`: + return await getRenderTree(this); + case `getBottomLeft`: + return await getOffset(this, args[0], {offsetType: `bottomLeft`}); + case `getBottomRight`: + return await getOffset(this, args[0], {offsetType: `bottomRight`}); + case `getCenter`: + return await getOffset(this, args[0], {offsetType: `center`}); + case `getTopLeft`: + return await getOffset(this, args[0], {offsetType: `topLeft`}); + case `getTopRight`: + return await getOffset(this, args[0], {offsetType: `topRight`}); + case `getRenderObjectDiagnostics`: + return await getRenderObjectDiagnostics(this, args[0], args[1]); + case `getWidgetDiagnostics`: + return await getWidgetDiagnostics(this, args[0], args[1]); + case `getSemanticsId`: + return await getSemanticsId(this, args[0]); + case `waitForAbsent`: + return await waitForAbsent(this, args[0], args[1]); + case `waitFor`: + return await waitFor(this, args[0], args[1]); + case `waitForTappable`: + return await waitForTappable(this, args[0], args[1]); + case `scroll`: + return await scroll(this, args[0], args[1]); + case `scrollUntilVisible`: + return await scrollUntilVisible(this, args[0], args[1]); + case `scrollUntilTapable`: + return await scrollUntilTapable(this, args[0], args[1]); + case `scrollIntoView`: + return await scrollIntoView(this, args[0], args[1]); + case `setTextEntryEmulation`: + return await setTextEntryEmulation(this, args[0]); + case `enterText`: + return await enterText(this, args[0]); + case `requestData`: + return await requestData(this, args[0]); + case `longTap`: + return await longTap(this, args[0], args[1]); + case `waitForFirstFrame`: + return await waitForCondition(this, {conditionName: `FirstFrameRasterizedCondition`}); + case `setFrameSync`: + return await setFrameSync(this, args[0], args[1]); + case `clickElement`: + 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}"`); + } }; const flutterLaunchApp = async ( @@ -242,3 +245,11 @@ const dragAndDropWithCommandExtension = async ( }; return await self.socket!.executeSocketCommand(commandPayload); }; + +async function getTextWithCommandExtension(self: FlutterDriver, params: { findBy: string; }) { + const payload = { + command: 'getTextWithCommandExtension', + findBy: params.findBy, + }; + return await self.socket!.executeSocketCommand(payload); +} From c61a2b53f9c70860c8fe561bc60d1b28c7e243fb Mon Sep 17 00:00:00 2001 From: tan Date: Sat, 30 Nov 2024 23:50:16 +0700 Subject: [PATCH 2/4] update description for new command --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ddba7b2..4615683a 100644 --- a/README.md +++ b/README.md @@ -355,10 +355,13 @@ 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: [extended_commands.dart](extended_commands.dart) +- getTextWithCommandExtension: [get_text_command.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. @@ -374,7 +377,7 @@ void main() { } ``` -#### Simple example using `dragAndDropWithCommandExtension` command in Python +### Simple example using `dragAndDropWithCommandExtension` command in Python ```python # python @@ -395,6 +398,16 @@ payload = { driver.execute_script("flutter:dragAndDropWithCommandExtension", payload) ``` +#### Simple example using `getTextWithCommandExtension` command in nodejs + +```typescript +import {byValueKey} from "appium-flutter-finder"; +const payload = { + 'findBy': byValueKey('amount'), + }; +const result = await driver.execute('flutter:getTextWithCommandExtension', payload); +``` + 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. From f63731ee902a9c9fc97ad0db8ba20883b0302d41 Mon Sep 17 00:00:00 2001 From: tan Date: Sun, 1 Dec 2024 12:05:43 +0700 Subject: [PATCH 3/4] fix indentation and readme --- README.md | 5 +- driver/lib/commands/execute.ts | 157 ++++++++++++++++----------------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 4615683a..84464650 100644 --- a/README.md +++ b/README.md @@ -368,16 +368,17 @@ The entry point must include the `List?` commands argument in ```dart import 'extended_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 example using `dragAndDropWithCommandExtension` command in Python ```python # python diff --git a/driver/lib/commands/execute.ts b/driver/lib/commands/execute.ts index 0c6c26e7..00b7d227 100644 --- a/driver/lib/commands/execute.ts +++ b/driver/lib/commands/execute.ts @@ -20,78 +20,77 @@ export const execute = async function( throw new Error(`Command not support: "${rawCommand}"`); } - const command = matching[1].trim(); - - switch (command) { - case `launchApp`: - return await flutterLaunchApp(this, args[0], args[1]); - case `connectObservatoryWsUrl`: - return await connectObservatoryWsUrl(this); - case `getVMInfo`: - return await getVMInfo(this); - case `setIsolateId`: - return await setIsolateId(this, args[0]); - case `getIsolate`: - return await getIsolate(this, args[0]); - case `checkHealth`: - return await checkHealth(this); - case `clearTimeline`: - return await clearTimeline(this); - case `forceGC`: - return await forceGC(this); - case `getRenderTree`: - return await getRenderTree(this); - case `getBottomLeft`: - return await getOffset(this, args[0], {offsetType: `bottomLeft`}); - case `getBottomRight`: - return await getOffset(this, args[0], {offsetType: `bottomRight`}); - case `getCenter`: - return await getOffset(this, args[0], {offsetType: `center`}); - case `getTopLeft`: - return await getOffset(this, args[0], {offsetType: `topLeft`}); - case `getTopRight`: - return await getOffset(this, args[0], {offsetType: `topRight`}); - case `getRenderObjectDiagnostics`: - return await getRenderObjectDiagnostics(this, args[0], args[1]); - case `getWidgetDiagnostics`: - return await getWidgetDiagnostics(this, args[0], args[1]); - case `getSemanticsId`: - return await getSemanticsId(this, args[0]); - case `waitForAbsent`: - return await waitForAbsent(this, args[0], args[1]); - case `waitFor`: - return await waitFor(this, args[0], args[1]); - case `waitForTappable`: - return await waitForTappable(this, args[0], args[1]); - case `scroll`: - return await scroll(this, args[0], args[1]); - case `scrollUntilVisible`: - return await scrollUntilVisible(this, args[0], args[1]); - case `scrollUntilTapable`: - return await scrollUntilTapable(this, args[0], args[1]); - case `scrollIntoView`: - return await scrollIntoView(this, args[0], args[1]); - case `setTextEntryEmulation`: - return await setTextEntryEmulation(this, args[0]); - case `enterText`: - return await enterText(this, args[0]); - case `requestData`: - return await requestData(this, args[0]); - case `longTap`: - return await longTap(this, args[0], args[1]); - case `waitForFirstFrame`: - return await waitForCondition(this, {conditionName: `FirstFrameRasterizedCondition`}); - case `setFrameSync`: - return await setFrameSync(this, args[0], args[1]); - case `clickElement`: - 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}"`); - } + const command = matching[1].trim(); + switch (command) { + case `launchApp`: + return await flutterLaunchApp(this, args[0], args[1]); + case `connectObservatoryWsUrl`: + return await connectObservatoryWsUrl(this); + case `getVMInfo`: + return await getVMInfo(this); + case `setIsolateId`: + return await setIsolateId(this, args[0]); + case `getIsolate`: + return await getIsolate(this, args[0]); + case `checkHealth`: + return await checkHealth(this); + case `clearTimeline`: + return await clearTimeline(this); + case `forceGC`: + return await forceGC(this); + case `getRenderTree`: + return await getRenderTree(this); + case `getBottomLeft`: + return await getOffset(this, args[0], { offsetType: `bottomLeft` }); + case `getBottomRight`: + return await getOffset(this, args[0], { offsetType: `bottomRight` }); + case `getCenter`: + return await getOffset(this, args[0], { offsetType: `center` }); + case `getTopLeft`: + return await getOffset(this, args[0], { offsetType: `topLeft` }); + case `getTopRight`: + return await getOffset(this, args[0], { offsetType: `topRight` }); + case `getRenderObjectDiagnostics`: + return await getRenderObjectDiagnostics(this, args[0], args[1]); + case `getWidgetDiagnostics`: + return await getWidgetDiagnostics(this, args[0], args[1]); + case `getSemanticsId`: + return await getSemanticsId(this, args[0]); + case `waitForAbsent`: + return await waitForAbsent(this, args[0], args[1]); + case `waitFor`: + return await waitFor(this, args[0], args[1]); + case `waitForTappable`: + return await waitForTappable(this, args[0], args[1]); + case `scroll`: + return await scroll(this, args[0], args[1]); + case `scrollUntilVisible`: + return await scrollUntilVisible(this, args[0], args[1]); + case `scrollUntilTapable`: + return await scrollUntilTapable(this, args[0], args[1]); + case `scrollIntoView`: + return await scrollIntoView(this, args[0], args[1]); + case `setTextEntryEmulation`: + return await setTextEntryEmulation(this, args[0]); + case `enterText`: + return await enterText(this, args[0]); + case `requestData`: + return await requestData(this, args[0]); + case `longTap`: + return await longTap(this, args[0], args[1]); + case `waitForFirstFrame`: + return await waitForCondition(this, { conditionName: `FirstFrameRasterizedCondition`}); + case `setFrameSync`: + return await setFrameSync(this, args[0], args[1]); + case `clickElement`: + 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}"`); + } }; const flutterLaunchApp = async ( @@ -246,10 +245,10 @@ const dragAndDropWithCommandExtension = async ( return await self.socket!.executeSocketCommand(commandPayload); }; -async function getTextWithCommandExtension(self: FlutterDriver, params: { findBy: string; }) { - const payload = { - command: 'getTextWithCommandExtension', - findBy: params.findBy, - }; - return await self.socket!.executeSocketCommand(payload); -} +const getTextWithCommandExtension = async (self: FlutterDriver, params: { findBy: string; }) => { + const payload = { + command: 'getTextWithCommandExtension', + findBy: params.findBy, + }; + return await self.socket!.executeSocketCommand(payload); +}; From b1390315bfb8029c4742457f91efc4f5cb0b77f4 Mon Sep 17 00:00:00 2001 From: tan Date: Mon, 2 Dec 2024 09:21:36 +0700 Subject: [PATCH 4/4] 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!, + }; + } +}