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(fluttium): implement scroll and swipe actions #219

Merged
merged 10 commits into from
Mar 27, 2023
17 changes: 17 additions & 0 deletions example/flows/scrollable_list.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
description: Testing the scrollable list page
---
# Validate that the app page is displayed
- expectEnvironmentText:
# Go to the scrollable list page
- pressOn: "Scrollable List"
# Validate that the scrollable list page is displayed and the items are visible
- expectVisible: "Scrollable List"
- expectVisible: "List item \\d+"
# Scroll down to list item 100
- scroll:
in: "list_view"
until: "List item 99"
- expectVisible: "List item 9\\d"
# Return to the app page
- pressOn: "Back"
- expectEnvironmentText:
2 changes: 1 addition & 1 deletion example/flows/simple_menu_flow.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
description: Testing the progress page
description: Testing the simple menu page
---
# Validate that the app page is displayed
- expectEnvironmentText:
Expand Down
9 changes: 8 additions & 1 deletion example/lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:example/complex_text/complex_text.dart';
import 'package:example/counter/counter.dart';
import 'package:example/drawer/drawer.dart';
import 'package:example/progress/progress.dart';
import 'package:example/scrollable_list/view/scrollable_list.dart';
wolfenrain marked this conversation as resolved.
Show resolved Hide resolved
import 'package:example/simple_menu/simple_menu.dart';
import 'package:example/text/text.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -58,7 +59,13 @@ class AppView extends StatelessWidget {
SimpleMenuPage.route(),
),
child: const Text('Simple Menu'),
)
),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
ScrollableListPage.route(),
),
child: const Text('Scrollable List'),
),
],
),
);
Expand Down
1 change: 1 addition & 0 deletions example/lib/scrollable_list/scrollable_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'view/scrollable_list.dart';
34 changes: 34 additions & 0 deletions example/lib/scrollable_list/view/scrollable_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';

class ScrollableListPage extends StatefulWidget {
const ScrollableListPage({super.key});

static Route<void> route() {
return MaterialPageRoute<void>(
builder: (_) => const ScrollableListPage(),
);
}

@override
State<ScrollableListPage> createState() => _ScrollableListPageState();
}

class _ScrollableListPageState extends State<ScrollableListPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Scrollable List')),
body: Semantics(
label: 'list_view',
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('List item $index'),
);
},
),
),
);
}
}
28 changes: 20 additions & 8 deletions example/test/app/view/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import 'package:example/complex_text/complex_text.dart';
import 'package:example/counter/counter.dart';
import 'package:example/drawer/drawer.dart';
import 'package:example/progress/progress.dart';
import 'package:example/scrollable_list/view/scrollable_list.dart';
import 'package:example/simple_menu/simple_menu.dart';
import 'package:example/text/text.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('App', () {
testWidgets('renders AppView', (tester) async {
group('$App', () {
testWidgets('renders $AppView', (tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
expect(find.byType(AppView), findsOneWidget);
});

testWidgets('navigates to CounterPage when Counter button is tapped',
testWidgets('navigates to $CounterPage when Counter button is tapped',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Counter'));
Expand All @@ -23,7 +24,7 @@ void main() {
expect(find.byType(CounterPage), findsOneWidget);
});

testWidgets('navigates to Drawer when Drawer button is tapped',
testWidgets('navigates to $DrawerPage when Drawer button is tapped',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Drawer'));
Expand All @@ -32,7 +33,7 @@ void main() {
expect(find.byType(DrawerPage), findsOneWidget);
});

testWidgets('navigates to TextPage when Text button is tapped',
testWidgets('navigates to $TextPage when Text button is tapped',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Text'));
Expand All @@ -41,7 +42,7 @@ void main() {
expect(find.byType(TextPage), findsOneWidget);
});

testWidgets('navigates to ProgressPage when Progress button is tapped',
testWidgets('navigates to $ProgressPage when Progress button is tapped',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Progress'));
Expand All @@ -51,7 +52,7 @@ void main() {
});

testWidgets(
'navigates to ComplexTextPage when Complex Text button is tapped',
'navigates to $ComplexTextPage when Complex Text button is tapped',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Complex Text'));
Expand All @@ -60,13 +61,24 @@ void main() {
expect(find.byType(ComplexTextPage), findsOneWidget);
});

testWidgets('navigates to SimpleMenuPage when Simple Menu button is tapped',
testWidgets(
'navigates to $SimpleMenuPage when Simple Menu button is tapped',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Simple Menu'));
await tester.pumpAndSettle();

expect(find.byType(SimpleMenuPage), findsOneWidget);
});

testWidgets(
'''navigates to $ScrollableListPage when Scrollable List button is tapped''',
(tester) async {
await tester.pumpWidget(const App(environment: 'Testing'));
await tester.tap(find.text('Scrollable List'));
await tester.pumpAndSettle();

expect(find.byType(ScrollableListPage), findsOneWidget);
});
});
}
22 changes: 22 additions & 0 deletions example/test/scrollable_list/view/scrollable_list_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:example/scrollable_list/view/scrollable_list.dart';
import 'package:flutter_test/flutter_test.dart';

import '../../helpers/helpers.dart';

void main() {
group('$ScrollableListPage', () {
testWidgets('renders $ScrollableListPage', (tester) async {
await tester.pumpApp(const ScrollableListPage());

expect(find.byType(ScrollableListPage), findsOneWidget);
});

testWidgets('renders "List items"', (tester) async {
await tester.pumpApp(const ScrollableListPage());

expect(find.text('List item 1'), findsOneWidget);
expect(find.text('List item 2'), findsOneWidget);
expect(find.text('List item 3'), findsOneWidget);
});
});
}
2 changes: 2 additions & 0 deletions packages/fluttium/lib/src/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export 'expect_not_visible.dart';
export 'expect_visible.dart';
export 'long_press_on.dart';
export 'press_on.dart';
export 'scroll.dart';
export 'swipe.dart';
export 'take_screenshot.dart';
export 'wait.dart';
export 'write_text.dart';
83 changes: 83 additions & 0 deletions packages/fluttium/lib/src/actions/scroll.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:clock/clock.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:fluttium/fluttium.dart';

/// {@template scroll}
/// Scroll within a node with a given direction until a child node is found.
///
/// This action can be invoked as followed:
/// ```yaml
/// - scroll:
/// within: "Your List View"
/// until: "Your List Item"
/// direction: up # Defaults to down
/// timeout: 5000 # Defaults to 10 seconds
/// ```
/// {@endtemplate}
class Scroll extends Action {
/// {@macro scroll}
Scroll({
required this.within,
required this.until,
int? timeout,
this.direction = AxisDirection.down,
}) : timeout = Duration(milliseconds: timeout ?? 100000);

/// The string to find the semantic node, in which the scrolling will happen.
final String within;

/// Scroll until the given semantic node is found.
final String until;

/// The direction of the scrolling.
final AxisDirection direction;

/// The time it will try to keep scrolling until it found the node.
final Duration timeout;

@override
Future<bool> execute(Tester tester) async {
final node = await tester.find(within);
if (node == null) {
return false;
}

final Offset scrollDelta;
switch (direction) {
case AxisDirection.up:
scrollDelta = const Offset(0, -40);
wolfenrain marked this conversation as resolved.
Show resolved Hide resolved
break;
case AxisDirection.right:
scrollDelta = const Offset(40, 0);
break;
case AxisDirection.down:
scrollDelta = const Offset(0, 40);
break;
case AxisDirection.left:
scrollDelta = const Offset(-40, 0);
break;
}

final end = clock.now().add(timeout);
while ((await tester.find(until, timeout: Duration.zero)) == null) {
tester.emitPointerEvent(
PointerScrollEvent(
position: node.center,
scrollDelta: scrollDelta,
),
);
await tester.pump();
if (clock.now().isAfter(end)) {
return false;
}
}

return true;
}

@override
String description() {
return 'Scroll ${direction.name} in "$within" until "$until" is found';
}
}
35 changes: 35 additions & 0 deletions packages/fluttium/lib/src/actions/swipe.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/rendering.dart';
import 'package:fluttium/src/actions/scroll.dart';

/// {@template swipe}
/// Swipe left or right within a node until a child node is found.
///
/// This action can be invoked as followed:
/// ```yaml
/// - swipe:
/// within: "Your List View"
/// until: "Your List Item"
/// direction: right # Defaults to left
/// timeout: 5000 # Defaults to 10 seconds
/// ```
/// {@endtemplate}
class Swipe extends Scroll {
/// {@template swipe}
Swipe({
required super.within,
required super.until,
super.direction = AxisDirection.left,
super.timeout,
}) {
if ([AxisDirection.down, AxisDirection.up].contains(direction)) {
throw UnsupportedError(
'The direction "${direction.name}" is not supported for swiping',
);
}
}

@override
String description() {
return 'Swipe ${direction.name} in "$within" until "$until" is found';
}
}
35 changes: 35 additions & 0 deletions packages/fluttium/lib/src/registry.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:fluttium/fluttium.dart';

/// {@template registry}
Expand All @@ -21,6 +22,40 @@ class Registry {
ActionRegistration(ExpectNotVisible.new, shortHand: #text),
'takeScreenshot': ActionRegistration(TakeScreenshot.new, shortHand: #path),
'wait': ActionRegistration(Wait.new, shortHand: #milliseconds),
'scroll': ActionRegistration(
({
required String within,
required String until,
String direction = 'down',
int? timeout,
}) =>
Scroll(
within: within,
until: until,
direction: AxisDirection.values.firstWhere((e) => e.name == direction),
timeout: timeout,
),
aliases: const [
Alias(['in'], #within)
],
),
'swipe': ActionRegistration(
({
required String within,
required String until,
String direction = 'left',
int? timeout,
}) =>
Swipe(
within: within,
until: until,
direction: AxisDirection.values.firstWhere((e) => e.name == direction),
timeout: timeout,
),
aliases: const [
Alias(['in'], #within)
],
),
};

/// Map of all the action that are registered.
Expand Down
Loading