Skip to content

Commit

Permalink
feat: add getText enhancement for complex text widgets (#755)
Browse files Browse the repository at this point in the history
* feat: add getText enhancement for complex text widgets

* update description for new command

* fix indentation and readme

* update extension command files and readme
  • Loading branch information
tandt53 authored Dec 3, 2024
1 parent 2360638 commit ab20371
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 6 deletions.
47 changes: 41 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandExtension>?` 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"]
Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions driver/lib/commands/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`);
}
Expand Down Expand Up @@ -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);
};
68 changes: 68 additions & 0 deletions example/dart/drag_commands.dart
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, dynamic> toJson() {
return {
'success': success,
};
}
}


class DragCommandExtension extends CommandExtension {
@override
Future<Result> 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<String, String> params,
DeserializeFinderFactory finderFactory,
DeserializeCommandFactory commandFactory) {
return DragCommand.deserialize(params);
}
}
180 changes: 180 additions & 0 deletions example/dart/get_text_command.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic>) {
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<Result> 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<String, String> 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<String, String> params)
: base64Element = params['findBy']!;
}

class GetTextResult extends Result {
final bool success;
final Map<String, dynamic>? data;

const GetTextResult(this.success, {this.data});

@override
Map<String, dynamic> toJson() {
return <String, dynamic>{
'success': success,
if (data != null) ...data!,
};
}
}

0 comments on commit ab20371

Please sign in to comment.