Skip to content

Commit 4711bab

Browse files
authored
Merge pull request #3062 from leancodepl/fix/wait-for-at
Fix at(), last and first to wait for widgets to appear #1938
2 parents ea4d879 + 9fa5e70 commit 4711bab

7 files changed

Lines changed: 348 additions & 41 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'dart:async';
2+
3+
import 'package:e2e_app/keys.dart';
4+
import 'package:flutter/material.dart';
5+
6+
class AtFinderScreen extends StatefulWidget {
7+
const AtFinderScreen({super.key});
8+
9+
@override
10+
State<AtFinderScreen> createState() => _AtFinderScreenState();
11+
}
12+
13+
class _AtFinderScreenState extends State<AtFinderScreen> {
14+
var _isFirstItemVisible = false;
15+
var _isSecondItemVisible = false;
16+
var _firstItemTapped = 0;
17+
var _secondItemTapped = 0;
18+
19+
@override
20+
void initState() {
21+
super.initState();
22+
23+
unawaited(
24+
Future<void>.delayed(const Duration(seconds: 1)).then((_) {
25+
if (!mounted) {
26+
return;
27+
}
28+
29+
setState(() {
30+
_isFirstItemVisible = true;
31+
});
32+
}),
33+
);
34+
35+
unawaited(
36+
Future<void>.delayed(const Duration(seconds: 3)).then((_) {
37+
if (!mounted) {
38+
return;
39+
}
40+
41+
setState(() {
42+
_isSecondItemVisible = true;
43+
});
44+
}),
45+
);
46+
}
47+
48+
@override
49+
Widget build(BuildContext context) {
50+
return Scaffold(
51+
appBar: AppBar(title: const Text('at() finder')),
52+
body: ListView(
53+
children: [
54+
if (_isFirstItemVisible)
55+
AtFinderItem(
56+
onTap: () {
57+
setState(() {
58+
_firstItemTapped++;
59+
});
60+
},
61+
),
62+
if (_isSecondItemVisible)
63+
AtFinderItem(
64+
onTap: () {
65+
setState(() {
66+
_secondItemTapped++;
67+
});
68+
},
69+
),
70+
if (_secondItemTapped > 0)
71+
Text(
72+
'Second item tapped $_secondItemTapped',
73+
key: K.atFinderSecondItemTapped,
74+
),
75+
if (_firstItemTapped > 0)
76+
Text(
77+
'First item tapped $_firstItemTapped',
78+
key: K.atFinderFirstItemTapped,
79+
),
80+
],
81+
),
82+
);
83+
}
84+
}
85+
86+
class AtFinderItem extends StatelessWidget {
87+
const AtFinderItem({super.key, required this.onTap});
88+
89+
final VoidCallback onTap;
90+
91+
@override
92+
Widget build(BuildContext context) {
93+
return ListTile(key: K.atFinderItem, title: Text('Item'), onTap: onTap);
94+
}
95+
}

dev/e2e_app/lib/keys.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ class Keys {
1010
static const bottomText = Key('bottomText');
1111
static const backButton = Key('backButton');
1212

13+
// at() finder screen
14+
static const atFinderFirstItemTapped = Key('atFinderFirstItemTapped');
15+
static const atFinderItem = Key('atFinderItem');
16+
static const atFinderScreenButton = Key('atFinderScreenButton');
17+
static const atFinderSecondItemTapped = Key('atFinderSecondItemTapped');
18+
1319
// autofocus text field flow
1420
static const usernameTextField = Key('usernameTextField');
1521
static const usernameNextButton = Key('usernameNextButton');
@@ -82,3 +88,7 @@ class Keys {
8288
static const smallImagePreview = Key('smallImagePreview');
8389
static const selectedPhotosCount = Key('selectedPhotosCount');
8490
}
91+
92+
class AtFinderItemKey extends ValueKey<int> {
93+
const AtFinderItemKey(super.value);
94+
}

dev/e2e_app/lib/main.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:app_links/app_links.dart';
22
import 'package:e2e_app/applink_screen.dart';
3+
import 'package:e2e_app/at_finder_screen.dart';
34
import 'package:e2e_app/camera_screen.dart';
45
import 'package:e2e_app/keys.dart';
56
import 'package:e2e_app/loading_screen.dart';
@@ -212,6 +213,13 @@ class _ExampleHomePageState extends State<ExampleHomePage> {
212213
),
213214
child: const Text('Open loading screen'),
214215
),
216+
TextButton(
217+
key: K.atFinderScreenButton,
218+
onPressed: () => Navigator.of(context).push(
219+
MaterialPageRoute<void>(builder: (_) => const AtFinderScreen()),
220+
),
221+
child: const Text('Open at() finder screen'),
222+
),
215223
TextButton(
216224
onPressed: () => Navigator.of(context).push(
217225
MaterialPageRoute<void>(builder: (_) => const LocationScreen()),
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:e2e_app/keys.dart';
2+
3+
import 'common.dart';
4+
5+
void main() {
6+
patrol(
7+
'at() waits until widget at index exists',
8+
($) async {
9+
await createApp($);
10+
await $(K.atFinderScreenButton).scrollTo().tap();
11+
12+
await $(K.atFinderItem).at(1).tap();
13+
await $(K.atFinderItem).first.tap();
14+
await $(K.atFinderItem).last.tap();
15+
16+
await $(K.atFinderFirstItemTapped).waitUntilVisible();
17+
await $(K.atFinderSecondItemTapped).waitUntilVisible();
18+
expect($(K.atFinderSecondItemTapped).text, 'Second item tapped 2');
19+
},
20+
tags: ['android', 'emulator', 'ios', 'simulator'],
21+
);
22+
}

packages/patrol_finders/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Unreleased
22

3+
- Fix `at()`, `first` and `last` to wait for widget to become visible.
34
- Add `isAndroid`, `isIOS`, `isWeb` and `isMacOS` getters.
45
- Remove `dart:io`.
56

packages/patrol_finders/lib/src/custom_finders/patrol_finder.dart

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -541,20 +541,52 @@ class PatrolFinder implements MatchFinder {
541541

542542
@override
543543
PatrolFinder get first {
544-
// TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548)
545-
return PatrolFinder(tester: tester, finder: finder.first);
544+
return _select(
545+
description: 'first',
546+
selector: (candidates) => candidates.take(1),
547+
);
546548
}
547549

548550
@override
549551
PatrolFinder get last {
550-
// TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548)
551-
return PatrolFinder(tester: tester, finder: finder.last);
552+
return _select(
553+
description: 'last',
554+
selector: (candidates) {
555+
if (candidates.isEmpty) {
556+
return const Iterable<Element>.empty();
557+
}
558+
559+
return [candidates.last] as Iterable<Element>;
560+
},
561+
);
552562
}
553563

554564
@override
555565
PatrolFinder at(int index) {
556-
// TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548)
557-
return PatrolFinder(tester: tester, finder: finder.at(index));
566+
return _select(
567+
description: 'index $index',
568+
selector: (candidates) {
569+
if (index < 0) {
570+
return const Iterable<Element>.empty();
571+
}
572+
573+
return candidates.skip(index).take(1);
574+
},
575+
);
576+
}
577+
578+
PatrolFinder _select({
579+
required String description,
580+
required Iterable<Element> Function(Iterable<Element> candidates) selector,
581+
}) {
582+
return PatrolFinder(
583+
tester: tester,
584+
finder: _PatrolSelectorFinder(
585+
finder,
586+
selectorDescription: description,
587+
selector: selector,
588+
),
589+
);
558590
}
559591

560592
@override
@@ -611,6 +643,30 @@ class PatrolFinder implements MatchFinder {
611643
bool precache() => finder.precache();
612644
}
613645

646+
class _PatrolSelectorFinder extends ChainedFinder {
647+
_PatrolSelectorFinder(
648+
super.parent, {
649+
required this.selectorDescription,
650+
required this.selector,
651+
});
652+
653+
final String selectorDescription;
654+
final Iterable<Element> Function(Iterable<Element> candidates) selector;
655+
656+
@override
657+
Iterable<Element> filter(Iterable<Element> parentCandidates) =>
658+
selector(parentCandidates);
659+
660+
@override
661+
String describeMatch(Plurality plurality) {
662+
return '${parent.describeMatch(plurality)} '
663+
'(ignoring all but $selectorDescription)';
664+
}
665+
666+
@override
667+
String get description => describeMatch(Plurality.many);
668+
}
669+
614670
/// Useful methods that make chained finders more readable.
615671
extension ActionCombiner on Future<PatrolFinder> {
616672
/// Same as [PatrolFinder.tap], but on a [PatrolFinder] which is not yet

0 commit comments

Comments
 (0)