Skip to content
95 changes: 95 additions & 0 deletions dev/e2e_app/lib/at_finder_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'dart:async';

import 'package:e2e_app/keys.dart';
import 'package:flutter/material.dart';

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

@override
State<AtFinderScreen> createState() => _AtFinderScreenState();
}

class _AtFinderScreenState extends State<AtFinderScreen> {
var _isFirstItemVisible = false;
var _isSecondItemVisible = false;
var _firstItemTapped = 0;
var _secondItemTapped = 0;

@override
void initState() {
super.initState();

unawaited(
Future<void>.delayed(const Duration(seconds: 1)).then((_) {
if (!mounted) {
return;
}

setState(() {
_isFirstItemVisible = true;
});
}),
);

unawaited(
Future<void>.delayed(const Duration(seconds: 3)).then((_) {
if (!mounted) {
return;
}

setState(() {
_isSecondItemVisible = true;
});
}),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('at() finder')),
body: ListView(
children: [
if (_isFirstItemVisible)
AtFinderItem(
onTap: () {
setState(() {
_firstItemTapped++;
});
},
),
if (_isSecondItemVisible)
AtFinderItem(
onTap: () {
setState(() {
_secondItemTapped++;
});
},
),
if (_secondItemTapped > 0)
Text(
'Second item tapped $_secondItemTapped',
key: K.atFinderSecondItemTapped,
),
if (_firstItemTapped > 0)
Text(
'First item tapped $_firstItemTapped',
key: K.atFinderFirstItemTapped,
),
],
),
);
}
}

class AtFinderItem extends StatelessWidget {
const AtFinderItem({super.key, required this.onTap});

final VoidCallback onTap;

@override
Widget build(BuildContext context) {
return ListTile(key: K.atFinderItem, title: Text('Item'), onTap: onTap);
}
}
10 changes: 10 additions & 0 deletions dev/e2e_app/lib/keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ class Keys {
static const bottomText = Key('bottomText');
static const backButton = Key('backButton');

// at() finder screen
static const atFinderFirstItemTapped = Key('atFinderFirstItemTapped');
static const atFinderItem = Key('atFinderItem');
static const atFinderScreenButton = Key('atFinderScreenButton');
static const atFinderSecondItemTapped = Key('atFinderSecondItemTapped');

// autofocus text field flow
static const usernameTextField = Key('usernameTextField');
static const usernameNextButton = Key('usernameNextButton');
Expand Down Expand Up @@ -82,3 +88,7 @@ class Keys {
static const smallImagePreview = Key('smallImagePreview');
static const selectedPhotosCount = Key('selectedPhotosCount');
}

class AtFinderItemKey extends ValueKey<int> {
const AtFinderItemKey(super.value);
}
8 changes: 8 additions & 0 deletions dev/e2e_app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:app_links/app_links.dart';
import 'package:e2e_app/applink_screen.dart';
import 'package:e2e_app/at_finder_screen.dart';
import 'package:e2e_app/camera_screen.dart';
import 'package:e2e_app/keys.dart';
import 'package:e2e_app/loading_screen.dart';
Expand Down Expand Up @@ -212,6 +213,13 @@ class _ExampleHomePageState extends State<ExampleHomePage> {
),
child: const Text('Open loading screen'),
),
TextButton(
key: K.atFinderScreenButton,
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const AtFinderScreen()),
),
child: const Text('Open at() finder screen'),
),
TextButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const LocationScreen()),
Expand Down
22 changes: 22 additions & 0 deletions dev/e2e_app/patrol_test/at_finder_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:e2e_app/keys.dart';

import 'common.dart';

void main() {
patrol(
'at() waits until widget at index exists',
($) async {
await createApp($);
await $(K.atFinderScreenButton).scrollTo().tap();

await $(K.atFinderItem).at(1).tap();
await $(K.atFinderItem).first.tap();
await $(K.atFinderItem).last.tap();

await $(K.atFinderFirstItemTapped).waitUntilVisible();
await $(K.atFinderSecondItemTapped).waitUntilVisible();
expect($(K.atFinderSecondItemTapped).text, 'Second item tapped 2');
},
tags: ['android', 'emulator', 'ios', 'simulator'],
);
}
1 change: 1 addition & 0 deletions packages/patrol_finders/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Unreleased

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

Expand Down
Comment thread
dworik marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -541,20 +541,52 @@ class PatrolFinder implements MatchFinder {

@override
PatrolFinder get first {
// TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548)
return PatrolFinder(tester: tester, finder: finder.first);
return _select(
description: 'first',
selector: (candidates) => candidates.take(1),
);
}

@override
PatrolFinder get last {
// TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548)
return PatrolFinder(tester: tester, finder: finder.last);
return _select(
description: 'last',
selector: (candidates) {
if (candidates.isEmpty) {
return const Iterable<Element>.empty();
}

return [candidates.last] as Iterable<Element>;
},
);
}

@override
PatrolFinder at(int index) {
// TODO: Throw a better error (https://github.com/leancodepl/patrol/issues/548)
return PatrolFinder(tester: tester, finder: finder.at(index));
return _select(
description: 'index $index',
selector: (candidates) {
if (index < 0) {
return const Iterable<Element>.empty();
}

return candidates.skip(index).take(1);
},
);
}

PatrolFinder _select({
required String description,
required Iterable<Element> Function(Iterable<Element> candidates) selector,
}) {
return PatrolFinder(
tester: tester,
finder: _PatrolSelectorFinder(
finder,
selectorDescription: description,
selector: selector,
),
);
}

@override
Expand Down Expand Up @@ -611,6 +643,30 @@ class PatrolFinder implements MatchFinder {
bool precache() => finder.precache();
}

class _PatrolSelectorFinder extends ChainedFinder {
_PatrolSelectorFinder(
super.parent, {
required this.selectorDescription,
required this.selector,
});

final String selectorDescription;
final Iterable<Element> Function(Iterable<Element> candidates) selector;

@override
Iterable<Element> filter(Iterable<Element> parentCandidates) =>
selector(parentCandidates);

@override
String describeMatch(Plurality plurality) {
return '${parent.describeMatch(plurality)} '
'(ignoring all but $selectorDescription)';
}

@override
String get description => describeMatch(Plurality.many);
}

/// Useful methods that make chained finders more readable.
extension ActionCombiner on Future<PatrolFinder> {
/// Same as [PatrolFinder.tap], but on a [PatrolFinder] which is not yet
Expand Down
Loading
Loading