mirror of https://github.com/flutter/samples.git
Compass App: Search form screen (#2353)
Part of the implementation of the Compass App for the Architecture sample. **Merge to `compass-app`** This PR introduces the Search Form Screen, in which users can select a region, a date range and the number of guests. The feature is split in 5 different widgets, each one depending on the `SearchFormViewModel`. The architecture follows the same patterns implemented in the previous PR https://github.com/flutter/samples/pull/2342 TODO later on: - Error handling is yet not implemented, we need to introduce a "logger" and a way to handle error responses. - All repositories return local data, next steps include creating the dart server app. - The search query at the moment only takes the "continent" and not the dates and number of guests, that would be implemented later on as well. ## Demo [Screencast from 2024-07-12 14-30-48.webm](https://github.com/user-attachments/assets/afbcdd4e-617a-49cc-894c-8e082748e572) ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.mdpull/2389/head
parent
cfedff5a5c
commit
496b467485
@ -0,0 +1,12 @@
|
|||||||
|
class Continent {
|
||||||
|
/// e.g. 'Europe'
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// e.g. 'https://rstr.in/google/tripedia/TmR12QdlVTT'
|
||||||
|
final String imageUrl;
|
||||||
|
|
||||||
|
Continent({
|
||||||
|
required this.name,
|
||||||
|
required this.imageUrl,
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import '../../../utils/result.dart';
|
||||||
|
import '../../models/continent.dart';
|
||||||
|
|
||||||
|
/// Data source with all possible continents.
|
||||||
|
abstract class ContinentRepository {
|
||||||
|
/// Get complete list of continents.
|
||||||
|
Future<Result<List<Continent>>> getContinents();
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import '../../../utils/result.dart';
|
||||||
|
import '../../models/continent.dart';
|
||||||
|
import 'continent_repository.dart';
|
||||||
|
|
||||||
|
/// Local data source with all possible regions.
|
||||||
|
class ContinentRepositoryLocal implements ContinentRepository {
|
||||||
|
@override
|
||||||
|
Future<Result<List<Continent>>> getContinents() {
|
||||||
|
return Future.value(
|
||||||
|
Result.ok(
|
||||||
|
[
|
||||||
|
Continent(
|
||||||
|
name: 'Europe',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/TmR12QdlVTT',
|
||||||
|
),
|
||||||
|
Continent(
|
||||||
|
name: 'Asia',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/VJ8BXlQg8O1',
|
||||||
|
),
|
||||||
|
Continent(
|
||||||
|
name: 'South America',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/flm_-o1aI8e',
|
||||||
|
),
|
||||||
|
Continent(
|
||||||
|
name: 'Africa',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/-nzi8yFOBpF',
|
||||||
|
),
|
||||||
|
Continent(
|
||||||
|
name: 'North America',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/jlbgFDrSUVE',
|
||||||
|
),
|
||||||
|
Continent(
|
||||||
|
name: 'Oceania',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/vxyrDE-fZVL',
|
||||||
|
),
|
||||||
|
Continent(
|
||||||
|
name: 'Australia',
|
||||||
|
imageUrl: 'https://rstr.in/google/tripedia/z6vy6HeRyvZ',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,35 @@
|
|||||||
import '../ui/results/widgets/results_screen.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../ui/results/view_models/results_viewmodel.dart';
|
import '../ui/results/view_models/results_viewmodel.dart';
|
||||||
|
import '../ui/results/widgets/results_screen.dart';
|
||||||
|
import '../ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
import '../ui/search_form/widgets/search_form_screen.dart';
|
||||||
|
|
||||||
/// Top go_router entry point
|
/// Top go_router entry point
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
initialLocation: '/results',
|
initialLocation: '/',
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/results',
|
path: '/',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final viewModel = ResultsViewModel(
|
final viewModel = SearchFormViewModel(continentRepository: context.read());
|
||||||
destinationRepository: context.read(),
|
return SearchFormScreen(viewModel: viewModel);
|
||||||
);
|
|
||||||
return ResultsScreen(viewModel: viewModel);
|
|
||||||
},
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'results',
|
||||||
|
builder: (context, state) {
|
||||||
|
final viewModel = ResultsViewModel(
|
||||||
|
destinationRepository: context.read(),
|
||||||
|
);
|
||||||
|
final parameters = state.uri.queryParameters;
|
||||||
|
// TODO: Pass the rest of query parameters to the ViewModel
|
||||||
|
viewModel.search(continent: parameters['continent']);
|
||||||
|
return ResultsScreen(viewModel: viewModel);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../themes/colors.dart';
|
||||||
|
|
||||||
|
/// Home button to navigate back to the '/' path.
|
||||||
|
class HomeButton extends StatelessWidget {
|
||||||
|
const HomeButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 40.0,
|
||||||
|
width: 40.0,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: AppColors.grey1),
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
onTap: () {
|
||||||
|
context.go('/');
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
size: 24.0,
|
||||||
|
Icons.home_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Custom scroll behavior to allow dragging with mouse.
|
||||||
|
/// Necessary to allow dragging with mouse on Continents carousel.
|
||||||
|
class AppCustomScrollBehavior extends MaterialScrollBehavior {
|
||||||
|
@override
|
||||||
|
Set<PointerDeviceKind> get dragDevices => {
|
||||||
|
PointerDeviceKind.touch,
|
||||||
|
// Allow to drag with mouse on Regions carousel
|
||||||
|
PointerDeviceKind.mouse,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../themes/colors.dart';
|
||||||
|
import 'home_button.dart';
|
||||||
|
|
||||||
|
/// Application top search bar.
|
||||||
|
///
|
||||||
|
/// Displays a search bar with the current query.
|
||||||
|
/// Includes [HomeButton] to navigate back to the '/' path.
|
||||||
|
class AppSearchBar extends StatelessWidget {
|
||||||
|
const AppSearchBar({
|
||||||
|
super.key,
|
||||||
|
this.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? query;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: AppColors.grey1),
|
||||||
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: query != null
|
||||||
|
? _QueryText(query: query!)
|
||||||
|
: const _EmptySearch(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const HomeButton(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QueryText extends StatelessWidget {
|
||||||
|
const _QueryText({
|
||||||
|
required this.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text(
|
||||||
|
query,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmptySearch extends StatelessWidget {
|
||||||
|
const _EmptySearch();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.search),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Search destination',
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
style: Theme.of(context).inputDecorationTheme.hintStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../../data/models/continent.dart';
|
||||||
|
import '../../../data/repositories/continent/continent_repository.dart';
|
||||||
|
import '../../../utils/result.dart';
|
||||||
|
|
||||||
|
final _dateFormat = DateFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
|
/// View model for the search form.
|
||||||
|
///
|
||||||
|
/// Contains the form selected options
|
||||||
|
/// and the logic to load the list of regions.
|
||||||
|
class SearchFormViewModel extends ChangeNotifier {
|
||||||
|
SearchFormViewModel({
|
||||||
|
required ContinentRepository continentRepository,
|
||||||
|
}) : _continentRepository = continentRepository {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
final ContinentRepository _continentRepository;
|
||||||
|
List<Continent> _continents = [];
|
||||||
|
String? _selectedContinent;
|
||||||
|
DateTimeRange? _dateRange;
|
||||||
|
int _guests = 0;
|
||||||
|
|
||||||
|
/// True if the form is valid and can be submitted
|
||||||
|
bool get valid =>
|
||||||
|
_guests > 0 && _selectedContinent != null && _dateRange != null;
|
||||||
|
|
||||||
|
/// Returns the search query string to call the Results screen
|
||||||
|
/// e.g. 'destination=Europe&checkIn=2024-05-09&checkOut=2024-05-24&guests=1',
|
||||||
|
/// Must be called only if [valid] is true
|
||||||
|
get searchQuery {
|
||||||
|
assert(valid, "Called searchQuery when the form is not valid");
|
||||||
|
assert(_selectedContinent != null, "Called searchQuery without a continent");
|
||||||
|
assert(_dateRange != null, "Called searchQuery without a date range");
|
||||||
|
assert(_guests > 0, "Called searchQuery without guests");
|
||||||
|
final startDate = _dateRange!.start;
|
||||||
|
final endDate = _dateRange!.end;
|
||||||
|
final uri = Uri(queryParameters: {
|
||||||
|
'continent': _selectedContinent!,
|
||||||
|
'checkIn': _dateFormat.format(startDate),
|
||||||
|
'checkOut': _dateFormat.format(endDate),
|
||||||
|
'guests': _guests.toString(),
|
||||||
|
});
|
||||||
|
return uri.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of continents.
|
||||||
|
/// Loaded in [load] method.
|
||||||
|
List<Continent> get continents => _continents;
|
||||||
|
|
||||||
|
/// Load the list of continents.
|
||||||
|
Future<void> load() async {
|
||||||
|
final result = await _continentRepository.getContinents();
|
||||||
|
switch (result) {
|
||||||
|
case Ok():
|
||||||
|
{
|
||||||
|
_continents = result.value;
|
||||||
|
}
|
||||||
|
case Error():
|
||||||
|
{
|
||||||
|
// TODO: Handle error
|
||||||
|
// ignore: avoid_print
|
||||||
|
print(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selected continent.
|
||||||
|
/// Null means no continent is selected.
|
||||||
|
String? get selectedContinent => _selectedContinent;
|
||||||
|
|
||||||
|
/// Set selected continent.
|
||||||
|
/// Set to null to clear the selection.
|
||||||
|
set selectedContinent(String? continent) {
|
||||||
|
_selectedContinent = continent;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Date range.
|
||||||
|
/// Null means no range is selected.
|
||||||
|
DateTimeRange? get dateRange => _dateRange;
|
||||||
|
|
||||||
|
/// Set date range.
|
||||||
|
/// Can be set to null to clear selection.
|
||||||
|
set dateRange(DateTimeRange? dateRange) {
|
||||||
|
_dateRange = dateRange;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of guests
|
||||||
|
int get guests => _guests;
|
||||||
|
|
||||||
|
/// Set number of guests
|
||||||
|
/// If the quantity is negative, it will be set to 0
|
||||||
|
set guests(int quantity) {
|
||||||
|
if (quantity < 0) {
|
||||||
|
_guests = 0;
|
||||||
|
} else {
|
||||||
|
_guests = quantity;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
import '../../../data/models/continent.dart';
|
||||||
|
import '../../core/themes/colors.dart';
|
||||||
|
import '../view_models/search_form_viewmodel.dart';
|
||||||
|
|
||||||
|
/// Continent selection carousel
|
||||||
|
///
|
||||||
|
/// Loads a list of continents in a horizontal carousel.
|
||||||
|
/// Users can tap one item to select it.
|
||||||
|
/// Tapping again the same item will deselect it.
|
||||||
|
class SearchFormContinent extends StatelessWidget {
|
||||||
|
const SearchFormContinent({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 140,
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: viewModel,
|
||||||
|
builder: (context, child) {
|
||||||
|
return ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: viewModel.continents.length,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final Continent(:imageUrl, :name) = viewModel.continents[index];
|
||||||
|
return _CarouselItem(
|
||||||
|
key: ValueKey(name),
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
name: name,
|
||||||
|
viewModel: viewModel,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
|
return const SizedBox(width: 8);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CarouselItem extends StatelessWidget {
|
||||||
|
const _CarouselItem({
|
||||||
|
super.key,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.name,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String imageUrl;
|
||||||
|
final String name;
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
bool _selected() =>
|
||||||
|
viewModel.selectedContinent == null ||
|
||||||
|
viewModel.selectedContinent == name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorWidget: (context, url, error) {
|
||||||
|
// NOTE: Getting "invalid image data" error for some of the images
|
||||||
|
// e.g. https://rstr.in/google/tripedia/jlbgFDrSUVE
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.grey3,
|
||||||
|
),
|
||||||
|
child: SizedBox(width: 140, height: 140),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.openSans(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColors.white1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Overlay when other continent is selected
|
||||||
|
Positioned.fill(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _selected() ? 0 : 0.7,
|
||||||
|
duration: kThemeChangeDuration,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
// Support dark-mode
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Handle taps
|
||||||
|
Positioned.fill(
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (viewModel.selectedContinent == name) {
|
||||||
|
viewModel.selectedContinent = null;
|
||||||
|
} else {
|
||||||
|
viewModel.selectedContinent = name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../core/themes/colors.dart';
|
||||||
|
import '../view_models/search_form_viewmodel.dart';
|
||||||
|
|
||||||
|
final _dateFormatDay = DateFormat('d');
|
||||||
|
final _dateFormatDayMonth = DateFormat('d MMM');
|
||||||
|
|
||||||
|
/// Date selection form field.
|
||||||
|
///
|
||||||
|
/// Opens a date range picker dialog when tapped.
|
||||||
|
class SearchFormDate extends StatelessWidget {
|
||||||
|
const SearchFormDate({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24, left: 20, right: 20),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
|
onTap: () {
|
||||||
|
showDateRangePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
).then((dateRange) => viewModel.dateRange = dateRange);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: AppColors.grey1),
|
||||||
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'When',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: viewModel,
|
||||||
|
builder: (context, _) {
|
||||||
|
final dateRange = viewModel.dateRange;
|
||||||
|
if (dateRange != null) {
|
||||||
|
return Text(
|
||||||
|
_dateFormat(dateRange),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Text(
|
||||||
|
'Add Dates',
|
||||||
|
style: Theme.of(context).inputDecorationTheme.hintStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _dateFormat(DateTimeRange dateTimeRange) {
|
||||||
|
final start = dateTimeRange.start;
|
||||||
|
final end = dateTimeRange.end;
|
||||||
|
|
||||||
|
final dayMonthEnd = _dateFormatDayMonth.format(end);
|
||||||
|
|
||||||
|
if (start.month == end.month) {
|
||||||
|
final dayStart = _dateFormatDay.format(start);
|
||||||
|
return '$dayStart - $dayMonthEnd';
|
||||||
|
}
|
||||||
|
|
||||||
|
final dayMonthStart = _dateFormatDayMonth.format(start);
|
||||||
|
return '$dayMonthStart - $dayMonthEnd';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/themes/colors.dart';
|
||||||
|
import '../view_models/search_form_viewmodel.dart';
|
||||||
|
|
||||||
|
/// Number of guests selection form
|
||||||
|
///
|
||||||
|
/// Users can tap the Plus and Minus icons to increase or decrease
|
||||||
|
/// the number of guests.
|
||||||
|
class SearchFormGuests extends StatelessWidget {
|
||||||
|
const SearchFormGuests({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 24, left: 20, right: 20),
|
||||||
|
child: Container(
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: AppColors.grey1),
|
||||||
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Who',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
_QuantitySelector(viewModel),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuantitySelector extends StatelessWidget {
|
||||||
|
const _QuantitySelector(this.viewModel);
|
||||||
|
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 90,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
key: const ValueKey('remove_guests'),
|
||||||
|
onTap: () {
|
||||||
|
viewModel.guests--;
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.remove_circle_outline,
|
||||||
|
color: AppColors.grey3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListenableBuilder(
|
||||||
|
listenable: viewModel,
|
||||||
|
builder: (context, _) => Text(
|
||||||
|
viewModel.guests.toString(),
|
||||||
|
style: viewModel.guests == 0
|
||||||
|
? Theme.of(context).inputDecorationTheme.hintStyle
|
||||||
|
: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
key: const ValueKey('add_guests'),
|
||||||
|
onTap: () {
|
||||||
|
viewModel.guests++;
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.add_circle_outline,
|
||||||
|
color: AppColors.grey3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../core/ui/search_bar.dart';
|
||||||
|
import '../../results/widgets/results_screen.dart';
|
||||||
|
import '../view_models/search_form_viewmodel.dart';
|
||||||
|
import 'search_form_date.dart';
|
||||||
|
import 'search_form_guests.dart';
|
||||||
|
import 'search_form_continent.dart';
|
||||||
|
import 'search_form_submit.dart';
|
||||||
|
|
||||||
|
/// Search form screen
|
||||||
|
///
|
||||||
|
/// Displays a search form with continent, date and guests selection.
|
||||||
|
/// Tapping on the submit button opens the [ResultsScreen] screen
|
||||||
|
/// passing the search options as query parameters.
|
||||||
|
class SearchFormScreen extends StatelessWidget {
|
||||||
|
const SearchFormScreen({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 24,
|
||||||
|
),
|
||||||
|
child: AppSearchBar(),
|
||||||
|
),
|
||||||
|
SearchFormContinent(viewModel: viewModel),
|
||||||
|
SearchFormDate(viewModel: viewModel),
|
||||||
|
SearchFormGuests(viewModel: viewModel),
|
||||||
|
const Spacer(),
|
||||||
|
SearchFormSubmit(viewModel: viewModel),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../../results/widgets/results_screen.dart';
|
||||||
|
import '../view_models/search_form_viewmodel.dart';
|
||||||
|
|
||||||
|
/// Search form submit button
|
||||||
|
///
|
||||||
|
/// The button is disabled when the form is data is incomplete.
|
||||||
|
/// When tapped, it navigates to the [ResultsScreen]
|
||||||
|
/// passing the search options as query parameters.
|
||||||
|
class SearchFormSubmit extends StatelessWidget {
|
||||||
|
const SearchFormSubmit({
|
||||||
|
super.key,
|
||||||
|
required this.viewModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: viewModel,
|
||||||
|
child: const SizedBox(
|
||||||
|
height: 52,
|
||||||
|
child: Center(
|
||||||
|
child: Text('Search'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context, child) {
|
||||||
|
return FilledButton(
|
||||||
|
key: const ValueKey('submit_button'),
|
||||||
|
onPressed: viewModel.valid
|
||||||
|
? () => context.go('/results?${viewModel.searchQuery}')
|
||||||
|
: null,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:compass_app/ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
|
||||||
|
import '../../../util/fakes/repositories/fake_continent_repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchFormViewModel Tests', () {
|
||||||
|
late SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = SearchFormViewModel(continentRepository: FakeContinentRepository());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Initial values are correct', () {
|
||||||
|
expect(viewModel.valid, false);
|
||||||
|
expect(viewModel.selectedContinent, null);
|
||||||
|
expect(viewModel.dateRange, null);
|
||||||
|
expect(viewModel.guests, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Setting dateRange updates correctly', () {
|
||||||
|
final DateTimeRange newDateRange = DateTimeRange(
|
||||||
|
start: DateTime(2024, 1, 1),
|
||||||
|
end: DateTime(2024, 1, 31),
|
||||||
|
);
|
||||||
|
viewModel.dateRange = newDateRange;
|
||||||
|
expect(viewModel.dateRange, newDateRange);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Setting selectedContinent updates correctly', () {
|
||||||
|
viewModel.selectedContinent = 'CONTINENT';
|
||||||
|
expect(viewModel.selectedContinent, 'CONTINENT');
|
||||||
|
|
||||||
|
// Setting null should work
|
||||||
|
viewModel.selectedContinent = null;
|
||||||
|
expect(viewModel.selectedContinent, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Setting guests updates correctly', () {
|
||||||
|
viewModel.guests = 2;
|
||||||
|
expect(viewModel.guests, 2);
|
||||||
|
|
||||||
|
// Guests number should not be negative
|
||||||
|
viewModel.guests = -1;
|
||||||
|
expect(viewModel.guests, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Set all values and obtain query', () {
|
||||||
|
expect(viewModel.valid, false);
|
||||||
|
|
||||||
|
viewModel.guests = 2;
|
||||||
|
viewModel.selectedContinent = 'CONTINENT';
|
||||||
|
final DateTimeRange newDateRange = DateTimeRange(
|
||||||
|
start: DateTime(2024, 1, 1),
|
||||||
|
end: DateTime(2024, 1, 31),
|
||||||
|
);
|
||||||
|
viewModel.dateRange = newDateRange;
|
||||||
|
|
||||||
|
expect(viewModel.valid, true);
|
||||||
|
expect(viewModel.searchQuery, 'continent=CONTINENT&checkIn=2024-01-01&checkOut=2024-01-31&guests=2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:compass_app/ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/search_form/widgets/search_form_continent.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||||
|
|
||||||
|
import '../../../util/fakes/repositories/fake_continent_repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchFormContinent widget tests', () {
|
||||||
|
late SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = SearchFormViewModel(
|
||||||
|
continentRepository: FakeContinentRepository(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
loadWidget(WidgetTester tester) async {
|
||||||
|
await mockNetworkImages(() async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: SearchFormContinent(
|
||||||
|
viewModel: viewModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('Should load and select continent', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormContinent), findsOneWidget);
|
||||||
|
|
||||||
|
// Select continent
|
||||||
|
await tester.tap(find.text('CONTINENT'), warnIfMissed: false);
|
||||||
|
|
||||||
|
expect(viewModel.selectedContinent, 'CONTINENT');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
import 'package:compass_app/ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/search_form/widgets/search_form_date.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../util/fakes/repositories/fake_continent_repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchFormDate widget tests', () {
|
||||||
|
late SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = SearchFormViewModel(
|
||||||
|
continentRepository: FakeContinentRepository(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
loadWidget(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: SearchFormDate(
|
||||||
|
viewModel: viewModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('should display date in different month', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormDate), findsOneWidget);
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
expect(find.text('Add Dates'), findsOneWidget);
|
||||||
|
|
||||||
|
// Simulate date picker input:
|
||||||
|
viewModel.dateRange = DateTimeRange(start: DateTime(2024, 6, 12), end: DateTime(2024, 7, 23));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('12 Jun - 23 Jul'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should display date in same month', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormDate), findsOneWidget);
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
expect(find.text('Add Dates'), findsOneWidget);
|
||||||
|
|
||||||
|
// Simulate date picker input:
|
||||||
|
viewModel.dateRange = DateTimeRange(start: DateTime(2024, 6, 12), end: DateTime(2024, 6, 23));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('12 - 23 Jun'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import 'package:compass_app/ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/search_form/widgets/search_form_guests.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../util/fakes/repositories/fake_continent_repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchFormGuests widget tests', () {
|
||||||
|
late SearchFormViewModel viewModel;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = SearchFormViewModel(
|
||||||
|
continentRepository: FakeContinentRepository(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
loadWidget(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: SearchFormGuests(
|
||||||
|
viewModel: viewModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('Increase number of guests', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormGuests), findsOneWidget);
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const ValueKey('add_guests')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Decrease number of guests', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormGuests), findsOneWidget);
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const ValueKey('remove_guests')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should remain at 0
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const ValueKey('add_guests')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Increase to 1
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const ValueKey('remove_guests')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Back to 0
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:compass_app/ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/search_form/widgets/search_form_screen.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:mocktail_image_network/mocktail_image_network.dart';
|
||||||
|
|
||||||
|
import '../../../util/mocks.dart';
|
||||||
|
import '../../../util/fakes/repositories/fake_continent_repository.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchFormScreen widget tests', () {
|
||||||
|
late SearchFormViewModel viewModel;
|
||||||
|
late MockGoRouter goRouter;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = SearchFormViewModel(
|
||||||
|
continentRepository: FakeContinentRepository(),
|
||||||
|
);
|
||||||
|
goRouter = MockGoRouter();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadWidget(WidgetTester tester) async {
|
||||||
|
await mockNetworkImages(() async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: InheritedGoRouter(
|
||||||
|
goRouter: goRouter,
|
||||||
|
child: Scaffold(
|
||||||
|
body: SearchFormScreen(
|
||||||
|
viewModel: viewModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('Should fill form and perform search', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormScreen), findsOneWidget);
|
||||||
|
|
||||||
|
// Select continent
|
||||||
|
await tester.tap(find.text('CONTINENT'), warnIfMissed: false);
|
||||||
|
|
||||||
|
// Select date
|
||||||
|
viewModel.dateRange = DateTimeRange(start: DateTime(2024, 6, 12), end: DateTime(2024, 7, 23));
|
||||||
|
|
||||||
|
// Select guests
|
||||||
|
await tester.tap(find.byKey(const ValueKey('add_guests')));
|
||||||
|
|
||||||
|
// Refresh screen state
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Perform search
|
||||||
|
await tester.tap(find.byKey(const ValueKey('submit_button')));
|
||||||
|
|
||||||
|
// Should navigate to results screen
|
||||||
|
verify(() => goRouter.go('/results?continent=CONTINENT&checkIn=2024-06-12&checkOut=2024-07-23&guests=1')).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
import 'package:compass_app/ui/search_form/view_models/search_form_viewmodel.dart';
|
||||||
|
import 'package:compass_app/ui/search_form/widgets/search_form_submit.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../../util/fakes/repositories/fake_continent_repository.dart';
|
||||||
|
import '../../../util/mocks.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SearchFormSubmit widget tests', () {
|
||||||
|
late SearchFormViewModel viewModel;
|
||||||
|
late MockGoRouter goRouter;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
viewModel = SearchFormViewModel(
|
||||||
|
continentRepository: FakeContinentRepository(),
|
||||||
|
);
|
||||||
|
goRouter = MockGoRouter();
|
||||||
|
});
|
||||||
|
|
||||||
|
loadWidget(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: InheritedGoRouter(
|
||||||
|
goRouter: goRouter,
|
||||||
|
child: Scaffold(
|
||||||
|
body: SearchFormSubmit(
|
||||||
|
viewModel: viewModel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('Should be enabled and allow tap', (WidgetTester tester) async {
|
||||||
|
await loadWidget(tester);
|
||||||
|
expect(find.byType(SearchFormSubmit), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap should not navigate
|
||||||
|
await tester.tap(find.byKey(const ValueKey('submit_button')));
|
||||||
|
verifyNever(() => goRouter.go(any()));
|
||||||
|
|
||||||
|
// Fill in data
|
||||||
|
viewModel.guests = 2;
|
||||||
|
viewModel.selectedContinent = 'CONTINENT';
|
||||||
|
final DateTimeRange newDateRange = DateTimeRange(
|
||||||
|
start: DateTime(2024, 1, 1),
|
||||||
|
end: DateTime(2024, 1, 31),
|
||||||
|
);
|
||||||
|
viewModel.dateRange = newDateRange;
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Perform search
|
||||||
|
await tester.tap(find.byKey(const ValueKey('submit_button')));
|
||||||
|
|
||||||
|
// Should navigate to results screen
|
||||||
|
verify(() => goRouter.go(
|
||||||
|
'/results?continent=CONTINENT&checkIn=2024-01-01&checkOut=2024-01-31&guests=2'))
|
||||||
|
.called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:compass_app/data/models/continent.dart';
|
||||||
|
import 'package:compass_app/data/repositories/continent/continent_repository.dart';
|
||||||
|
import 'package:compass_app/utils/result.dart';
|
||||||
|
|
||||||
|
class FakeContinentRepository implements ContinentRepository {
|
||||||
|
@override
|
||||||
|
Future<Result<List<Continent>>> getContinents() async {
|
||||||
|
return Result.ok([
|
||||||
|
Continent(name: 'CONTINENT', imageUrl: 'URL'),
|
||||||
|
Continent(name: 'CONTINENT2', imageUrl: 'URL'),
|
||||||
|
Continent(name: 'CONTINENT3', imageUrl: 'URL'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockGoRouter extends Mock implements GoRouter {}
|
Loading…
Reference in new issue