Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getText enhancement for complex text widgets #755

Merged
merged 4 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
tandt53 marked this conversation as resolved.
Show resolved Hide resolved

# 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!,
};
}
}
Loading