From 05100f0f3267d25aaa77cba20cabe1855a6cd69b Mon Sep 17 00:00:00 2001 From: Fakhira Devina <fakhira.devina@ui.ac.id> Date: Tue, 7 Apr 2020 00:04:11 +0700 Subject: [PATCH] [CHORE] Pencarian Page Cookie is functional - test not done yet --- .flutter-plugins-dependencies | 2 +- .gitignore | 2 + .gitlab-ci.yml | 2 +- README.md | 15 +- lib/bloc/LokasiResponseBloc.dart | 60 +++++ lib/config/strings.dart | 10 +- lib/model/lokasi.dart | 25 +++ lib/model/lokasi.g.dart | 40 ++++ lib/network/CustomException.dart | 27 +++ lib/network/cookies_interface.dart | 98 +++++++++ lib/network/data/network_model.dart | 16 ++ lib/network/network_interface.dart | 94 ++++++++ lib/page/dashboard/dashboard.dart | 3 +- lib/page/pencarian/pencarian.dart | 314 +++++++++++++++++---------- lib/repository/LokasiRepository.dart | 36 +++ pubspec.yaml | 5 +- test/cookie_test.dart | 63 ++++++ test/mock_test.dart | 87 ++++++++ test/navigation_test.dart | 65 ------ test/pencarian_test.dart | 50 ++++- test/widget_test.dart | 2 +- 21 files changed, 825 insertions(+), 191 deletions(-) create mode 100644 lib/bloc/LokasiResponseBloc.dart create mode 100644 lib/model/lokasi.dart create mode 100644 lib/model/lokasi.g.dart create mode 100644 lib/network/CustomException.dart create mode 100644 lib/network/cookies_interface.dart create mode 100644 lib/network/data/network_model.dart create mode 100644 lib/network/network_interface.dart create mode 100644 lib/repository/LokasiRepository.dart create mode 100644 test/cookie_test.dart create mode 100644 test/mock_test.dart delete mode 100644 test/navigation_test.dart diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index e49cab89..52b1c6a0 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_maps_flutter","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_maps_flutter-0.5.24+1\\\\","dependencies":[]},{"name":"location","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location-2.5.3\\\\","dependencies":[]}],"android":[{"name":"flutter_plugin_android_lifecycle","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_plugin_android_lifecycle-1.0.6\\\\","dependencies":[]},{"name":"google_maps_flutter","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_maps_flutter-0.5.24+1\\\\","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","path":"D:\\\\Flutter\\\\flutter_windows_v1.9.1+hotfix.2-stable\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location-2.5.3\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_maps_flutter","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","dependencies":[]}],"date_created":"2020-03-25 22:29:07.715265","version":"1.15.17"} \ No newline at end of file +{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_maps_flutter","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"location","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]}]} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 93baea49..a1b47e53 100644 --- a/.gitignore +++ b/.gitignore @@ -289,3 +289,5 @@ modules.xml # End of https://www.gitignore.io/api/linux,django,python,pycharm+all tests.output + +.flutter-plugins-dependencies diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c73806b7..da9a47c7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,8 +22,8 @@ Lint: Test: stage: test script: - - flutter test --machine > tests.output - flutter test --coverage + - flutter test --machine > tests.output - lcov --summary coverage/lcov.info - genhtml coverage/lcov.info --output=coverage artifacts: diff --git a/README.md b/README.md index 67bdfefd..6c0f425d 100644 --- a/README.md +++ b/README.md @@ -50,4 +50,17 @@ MAPS_API_KEY=Bu*************** Run the app using the development flavor ```bash flutter run -t lib/main_dev.dart -``` \ No newline at end of file +``` +## Building Models with JsonSerializable +Jadi abis get dari API, jsonnya di map ke models biar rapih. +1. Tulis ada field apa aja dari jsonnya (bisa liat contoh yang di models/lokasi.dart) +2. bagian 'part of {nama models}.g.dart' itu harus ditulis di model yg mau dibuat. di awal emang merah, tapi biarin aja +3. kalo semua field udah di tulis, run +```bash +flutter pub run build_runner build +``` +4. nanti akan ke build file {nama models}.g.dart, yang di nomor 2 merah harusnya udah gak merah lagi + +## Passing Data with BLoC +Udah ada contohnya di /bloc (implementasi di screen nya ada di page/pencarian/pencarian.dart) +Bisa baca [disini]https://itnext.io/flutter-handling-your-network-api-calls-like-a-boss-936eef296547 sebagai panduannya \ No newline at end of file diff --git a/lib/bloc/LokasiResponseBloc.dart b/lib/bloc/LokasiResponseBloc.dart new file mode 100644 index 00000000..ace530a1 --- /dev/null +++ b/lib/bloc/LokasiResponseBloc.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:ppl_disabilitas/model/lokasi.dart'; +import 'package:ppl_disabilitas/network/data/network_model.dart'; +import 'package:ppl_disabilitas/repository/LokasiRepository.dart'; + +class LokasiResponseBloc { + StreamController _recentSearchController; + LokasiRepository _lokasiRepository; + StreamController _lokasiListController; + + StreamSink<NetworkModel<LokasiListResponse>> get recentSearchSink => + _recentSearchController.sink; + Stream<NetworkModel<LokasiListResponse>> get recentSearchStream => + _recentSearchController.stream; + + StreamSink<NetworkModel<LokasiListResponse>> get lokasiListSink => + _lokasiListController.sink; + Stream<NetworkModel<LokasiListResponse>> get lokasiListStream => + _lokasiListController.stream; + + LokasiResponseBloc() { + _lokasiListController = + StreamController<NetworkModel<LokasiListResponse>>(); + _recentSearchController = StreamController<NetworkModel<LokasiListResponse>>(); + _lokasiRepository = LokasiRepository(); + fetchLokasiList(); + fetchRecentSearch(); + } + + fetchLokasiList() async { + lokasiListSink.add(NetworkModel.loading('Getting Locations')); + try { + LokasiListResponse lokasiListResponse = + await _lokasiRepository.fetchLokasi(); + lokasiListSink.add(NetworkModel.completed(lokasiListResponse)); + } catch (e) { + lokasiListSink.add(NetworkModel.error(e.toString())); + } + } + + fetchRecentSearch() async { + recentSearchSink.add(NetworkModel.loading('Getting Recent Search')); + try { + LokasiListResponse recentSearchData = await _lokasiRepository.fetchRecentSearch(); + recentSearchSink.add(NetworkModel.completed(recentSearchData)); + } catch (e) { + recentSearchSink.add(NetworkModel.error(e.toString())); + } + } + + saveRecentSearch(Lokasi search) async { + await _lokasiRepository.saveRecentSearch(search); + } + + dispose() { + _recentSearchController?.close(); + _lokasiListController?.close(); + } +} diff --git a/lib/config/strings.dart b/lib/config/strings.dart index f680bdbc..0162847d 100644 --- a/lib/config/strings.dart +++ b/lib/config/strings.dart @@ -5,12 +5,12 @@ final String devBaseURL = "poipole.herokuapp.com"; final String baseURL = "poipole.herokuapp.com"; String key = ""; String csrf = ""; -String sessionID = ""; +String sessionId = ""; -setKey(String key) { - key = key; +setKey(String newKey) { + key = newKey; } -setSessionId(String sessionId) { - sessionID = sessionId; +setSessionId(String newSessionId) { + sessionId = newSessionId; } \ No newline at end of file diff --git a/lib/model/lokasi.dart b/lib/model/lokasi.dart new file mode 100644 index 00000000..7981aec8 --- /dev/null +++ b/lib/model/lokasi.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'lokasi.g.dart'; +@JsonSerializable() +class LokasiListResponse { + List<Lokasi> listLokasi; + + LokasiListResponse(); + factory LokasiListResponse.fromJson(List json) => _$LokasiListResponseFromJson(json); + Map<String, dynamic> toJson() => _$LokasiListResponseToJson(this); +} + +@JsonSerializable(nullable: true) +class Lokasi { + String nama; + double latitude; + double longitude; + String alamat; + String foto; + String telp; + + Lokasi(); + + factory Lokasi.fromJson(Map<String, dynamic> json) => _$LokasiFromJson(json); + Map<String, dynamic> toJson() => _$LokasiToJson(this); +} diff --git a/lib/model/lokasi.g.dart b/lib/model/lokasi.g.dart new file mode 100644 index 00000000..a5ec8305 --- /dev/null +++ b/lib/model/lokasi.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'lokasi.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LokasiListResponse _$LokasiListResponseFromJson(List json) { + return LokasiListResponse() + ..listLokasi = json + ?.map((e) => + e == null ? null : Lokasi.fromJson(e as Map<String, dynamic>)) + ?.toList(); +} + +Map<String, dynamic> _$LokasiListResponseToJson(LokasiListResponse instance) => + <String, dynamic>{ + 'listLokasi': instance.listLokasi, + }; + + +Lokasi _$LokasiFromJson(Map<String, dynamic> json) { + return Lokasi() + ..nama = json['nama'] as String + ..latitude = (json['latitude'] as num)?.toDouble() + ..longitude = (json['longitude'] as num)?.toDouble() + ..alamat = json['alamat'] as String + ..foto = json['foto'] as String + ..telp = json['telp'] as String; +} + +Map<String, dynamic> _$LokasiToJson(Lokasi instance) => <String, dynamic>{ + 'nama': instance.nama, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'alamat': instance.alamat, + 'foto': instance.foto, + 'telp': instance.telp, +}; diff --git a/lib/network/CustomException.dart b/lib/network/CustomException.dart new file mode 100644 index 00000000..7d533760 --- /dev/null +++ b/lib/network/CustomException.dart @@ -0,0 +1,27 @@ +class CustomException implements Exception { + final _message; + final _prefix; + + CustomException([this._message, this._prefix]); + + String toString() { + return "$_prefix$_message"; + } +} + +class FetchDataException extends CustomException { + FetchDataException([String message]) + : super(message, "Error During Communication: "); +} + +class BadRequestException extends CustomException { + BadRequestException([message]) : super(message, "Invalid Request: "); +} + +class UnauthorisedException extends CustomException { + UnauthorisedException([message]) : super(message, "Unauthorised: "); +} + +class InvalidInputException extends CustomException { + InvalidInputException([String message]) : super(message, "Invalid Input: "); +} \ No newline at end of file diff --git a/lib/network/cookies_interface.dart b/lib/network/cookies_interface.dart new file mode 100644 index 00000000..09b6bf6b --- /dev/null +++ b/lib/network/cookies_interface.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; +import 'package:ppl_disabilitas/config/strings.dart'; + +class CookiesInterface { + Future<bool> checkCookieFileAvailability({String fileName}) async { + Directory dir; + await getApplicationDocumentsDirectory().then((Directory directory) { + dir = directory; + }); + File cookieFile = File("${dir.path}/$fileName.json"); + bool cookiesExist = cookieFile.existsSync(); + + return cookiesExist; + } + + Future<File> createSignInCookie({ + Map<String, dynamic> responseHeaders}) async { + try { + String setCookie; + String csrfToken; + String sessionId; + String userKey; + List<String> cookiesList; + Directory dir; + + await getApplicationDocumentsDirectory().then((Directory directory) { + dir = directory; + }); + File cookieFile = File("${dir.path}/usercookies.json"); + cookieFile.createSync(); + + setCookie = responseHeaders["set-cookie"]; + if (setCookie != null) { + csrfToken = setCookie.split(";")[0].split("=")[1]; + sessionId = setCookie.split(";")[4].split(",")[1].split("=")[1]; + userKey = key; + } + cookiesList = <String>[ + csrfToken, + sessionId, + userKey, + ]; + cookieFile.writeAsStringSync(json.encode(cookiesList)); + return cookieFile; + } on Exception catch (e) { + print(e.toString()); + rethrow; + } + } + + Future<File> createSearchHistoryCookie({ + Map<String, dynamic> recentSearch}) async { + print("recent searrch $recentSearch"); + Directory dir; + List currentSearchHistory; + try { + await getApplicationDocumentsDirectory().then((Directory directory) { + dir = directory; + }); + File cookieFile = File(dir.path + "/searchhistory.json"); + cookieFile.createSync(); + await checkCookieFileAvailability(fileName: "searchhistory").then((available) async { + if (available) { + await getCookieFile(fileName: "searchhistory").then((cookie) { + bool test = cookie == null; + print("cookie is null? $test"); + if (cookie == null) { + currentSearchHistory = []; + } else { + currentSearchHistory = json.decode(cookie); + } + currentSearchHistory.insert(0, recentSearch); + }); + } else { + currentSearchHistory = []; + } + await cookieFile.writeAsString(json.encode(currentSearchHistory)); + }); + return cookieFile; + } on Exception catch (e) { + print(e.toString()); + rethrow; + } + } + + Future<dynamic> getCookieFile({String fileName}) async { + Directory dir; + await getApplicationDocumentsDirectory().then((Directory directory) { + dir = directory; + }); + File file = File("${dir.path}/$fileName.json"); + dynamic res = file.readAsStringSync(); + return res; + } +} diff --git a/lib/network/data/network_model.dart b/lib/network/data/network_model.dart new file mode 100644 index 00000000..f42bffaf --- /dev/null +++ b/lib/network/data/network_model.dart @@ -0,0 +1,16 @@ +class NetworkModel<T> { + Status status; + T data; + String message; + + NetworkModel.loading(this.message) : status = Status.LOADING; + NetworkModel.completed(this.data) : status = Status.COMPLETED; + NetworkModel.error(this.message) : status = Status.ERROR; + + @override + String toString() { + return "Status : $status \n Message : $message \n Data : $data"; + } +} + +enum Status { LOADING, COMPLETED, ERROR } diff --git a/lib/network/network_interface.dart b/lib/network/network_interface.dart new file mode 100644 index 00000000..a451cf39 --- /dev/null +++ b/lib/network/network_interface.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'package:ppl_disabilitas/network/CustomException.dart'; +import 'package:http/http.dart' as http; +import 'dart:io'; + +class NetworkInterface { + //String key = KEY; + + // POST request + Future<dynamic> post({ + String url, //url nya apa + dynamic bodyParams, //data apa yang mau dikasih + bool isLogin, //dia login apa ngga + }) async { + var responseJson; + Map<String, String> headersJson = + await _buildRequestHeader(isLogin); //butuh header apa ngga + try { + final response = await http.post( + "$url", + body: json.encode(bodyParams), + headers: headersJson, + ); + responseJson = _response(response); + } on SocketException { + throw FetchDataException("No Internet Connection"); + } + return responseJson; + } + + // GET request + Future<dynamic> get({ + String url, + bool isLogin, + }) async { + var responseJson; + Map<String, dynamic> headersJson = await _buildRequestHeader(isLogin); + try { + final response = await http.get( + "$url", + headers: headersJson, + ); + responseJson = _response(response); + } on SocketException { + throw FetchDataException("No Internet Connection"); + } + return responseJson; + } + + // buildRequestHeader: untuk nentuin pake header apa aja berdasarkan login apa ngga + Future<Map<String, dynamic>> _buildRequestHeader(bool isLogin) async { + Map<String, String> headers = Map<String, String>(); + headers.putIfAbsent("Content-Type", () => "application/json"); + //if (isLogin) { + //List<dynamic> cookieFile = await CookiesInterface().getCookieFile( + // fileName: + // "userCookies"); //ngambil data dari yg udh disimpen di cookie + //print("cookieFile list --> ${cookieFile.toString()}"); + //print("check key here >>> $key"); + //setKey(cookieFile[2]); + //key = cookieFile[2]; + //headers.putIfAbsent( + // "Authorization", + // () => + // 'Token $key'); //ini kalau authorization nya ngga ada baru taro token nya + //headers.putIfAbsent("X-CSRFToken", () => cookieFile[0]); //csrf token + //headers.putIfAbsent( + // "Cookie", + // () => + // "csrftoken=${cookieFile[0]};sessionid=${cookieFile[1]}"); //cookie file + //print("headers --> ${headers}"); + //} + return headers; + } + + dynamic _response(http.Response response) { + switch (response.statusCode) { + case 200: + var responseJson = json.decode(response.body.toString()); + return responseJson; + case 400: + throw BadRequestException(response.body.toString()); + case 401: + + case 403: + throw UnauthorisedException(response.body.toString()); + case 500: + + default: + throw FetchDataException( + 'Error occured while Communication with Server with status : ${response.statusCode}'); + } + } +} diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index 1f4dcea1..a8c24ec0 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -46,6 +46,7 @@ class DashboardState extends State<Dashboard> { } void enableLocationService() async { + await location.changeSettings(accuracy: LocationAccuracy.HIGH); _serviceEnabled = await location.serviceEnabled(); if (!_serviceEnabled) { _serviceEnabled = await location.requestService(); @@ -72,7 +73,7 @@ class DashboardState extends State<Dashboard> { Widget build(BuildContext context) { return Scaffold( drawer: BisaGoDrawer(), - body: Stack(children: <Widget>[ + body: Stack(key: Key("Stack"),children: <Widget>[ _buildGoogleMap(context), InkWell( key: Key("Navigate to Pencarian"), diff --git a/lib/page/pencarian/pencarian.dart b/lib/page/pencarian/pencarian.dart index 5e4a680c..e9ee7709 100644 --- a/lib/page/pencarian/pencarian.dart +++ b/lib/page/pencarian/pencarian.dart @@ -1,14 +1,64 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:ppl_disabilitas/bloc/LokasiResponseBloc.dart'; import 'package:ppl_disabilitas/config/styles.dart'; +import 'package:ppl_disabilitas/model/lokasi.dart'; +import 'package:ppl_disabilitas/network/data/network_model.dart'; +/// Create Pencarian page widget with a state class Pencarian extends StatefulWidget { @override PencarianState createState() => PencarianState(); } +/// State of Pencacrian page class PencarianState extends State<Pencarian> { + /// Controller for textFormField + TextEditingController myController = TextEditingController(); + + /// Search Icon for textFormField Icon searchIcon = Icon(Icons.search); - Widget appBarText = Text("Pencarian Lokasi"); + + /// Text for appbar + Widget appBarText = Text('Pencarian Lokasi'); + + /// List of places currently searched on the textFormField + List<Lokasi> currentSearch = []; + + /// List for places from API + List<Lokasi> places = []; + + /// BLoC for pencarian + LokasiResponseBloc _bloc = LokasiResponseBloc(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty<List>('places', places)) + ..add(DiagnosticsProperty<List>('currentSearch', currentSearch)); + } + + @override + void initState() { + myController.addListener(() { + List<Lokasi> tempList = []; + for (int i = 0; i < places.length; i++) { + if (myController.text.isNotEmpty) { + if (places[i] + .nama + .toLowerCase() + .contains(myController.text.toLowerCase())) { + tempList.add(places[i]); + } + } + } + setState(() { + currentSearch = tempList; + }); + }); + super.initState(); + } @override Widget build(BuildContext context) { @@ -17,7 +67,7 @@ class PencarianState extends State<Pencarian> { backgroundColor: greenPrimary, leading: IconButton( icon: Icon(Icons.arrow_back_ios, size: 25), - key: Key("Back Icon Key"), + key: Key('Back Icon Key'), onPressed: () => Navigator.pop(context, 'Take me back')), title: Container( margin: EdgeInsets.only(top: doubleSpace, bottom: doubleSpace), @@ -26,7 +76,8 @@ class PencarianState extends State<Pencarian> { borderRadius: doubleBorderRadius, boxShadow: regularShadow), child: TextFormField( - key: Key("Text Field Mau Kemana"), + controller: myController, + key: Key('Text Field Mau Kemana'), decoration: InputDecoration( contentPadding: EdgeInsets.all(0), isDense: false, @@ -44,127 +95,170 @@ class PencarianState extends State<Pencarian> { fontFamily: 'Muli', fontWeight: FontWeight.w700), suffixIcon: IconButton( - icon: Icon( - Icons.mic, - color: greenPrimary, - size: 25, - ), - onPressed: () {})), + icon: Icon( + Icons.mic, + color: greenPrimary, + size: 25, + ), + onPressed: () {}, + )), ), ), ), - body: ListView( - padding: const EdgeInsets.all(8), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ - Padding( - padding: EdgeInsets.only(left: doubleSpace, top: 10), - child: Text( - 'Hasil Pencarian', - style: TextStyle( - fontSize: 15, - color: Colors.black, - fontFamily: 'Muli', - ), - ), - ), - Container( - height: 90, - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - CircleAvatar( - backgroundColor: greenPrimary, - child: Text('Test'), - ), - Padding( - padding: EdgeInsets.all(doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - Text( - 'Margo City', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w800, - color: Colors.black, - fontFamily: 'Muli', - ), - ), - Text( - 'Jl. Margonda Raya No.358, Kemir...', - style: TextStyle( - fontSize: 15, - color: Colors.black, - fontFamily: 'Muli', - ), + StreamBuilder<NetworkModel<LokasiListResponse>>( + stream: _bloc.recentSearchStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.LOADING: + return Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation<Color>(greenPrimary), ), - ], - ), - ), - Icon( - Icons.arrow_forward_ios, - color: Colors.grey[400], - size: 20, - ) - ], - ), + ); + break; + case Status.COMPLETED: + final recentSearch = snapshot.data.data; + Widget displayWidget; + if (recentSearch.listLokasi.isEmpty) { + displayWidget = Center( + child: Text("Anda belum pernah melakukan pencarian")); + } else { + displayWidget = makeLokasiWidget( + "history", recentSearch.listLokasi.take(3).toList()); + } + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Container( + margin: EdgeInsets.all(doubleSpace), + child: Text( + "Pencarian terdahulu", + style: + TextStyle(fontFamily: 'Muli', fontSize: 15), + )), + Flexible(child: displayWidget), + ], + )); + break; + case Status.ERROR: + return Center( + child: Text("${snapshot.data.status}"), + ); + break; + } + } + return Container(); + }, ), Container( - decoration: BoxDecoration( - border: Border(top: BorderSide(color: Colors.grey[400]))), + margin: EdgeInsets.only( + left: doubleSpace, top: regularSpace, bottom: smallSpace), + child: Text("Hasil Pencarian"), ), - Container( - height: 90, - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - CircleAvatar( - backgroundColor: greenPrimary, - child: Text('Test'), - ), - Padding( - padding: EdgeInsets.all(doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + StreamBuilder<NetworkModel<LokasiListResponse>>( + stream: _bloc.lokasiListStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.LOADING: + return Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation<Color>(greenPrimary), + ), + ); + break; + case Status.COMPLETED: + places = snapshot.data.data.listLokasi; + return Expanded( + flex: 2, + child: currentSearch.isEmpty + ? Center(child: Text('Cari lokasi')) + : makeLokasiWidget("api", currentSearch)); + break; + case Status.ERROR: + return Center( + child: Text(snapshot.data.data.toString()), + ); + break; + } + } + return Container(); + }, + ), + ], + ), + ); + } + + Widget makeLokasiWidget(String key, List<Lokasi> places) { + print('$key - $places'); + return ListView.builder( + shrinkWrap: true, + itemCount: places.length, + itemBuilder: (context, index) { + return InkWell( + key: Key("$key-${places[index].nama}"), + onTap: () { + _bloc.saveRecentSearch(places[index]); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + border: Border(bottom: BorderSide(color: Colors.grey[400]))), + margin: EdgeInsets.only(left: doubleSpace, right: doubleSpace), + height: 90, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Row( children: <Widget>[ - Text( - 'Margo City', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w800, - color: Colors.black, - fontFamily: 'Muli', - ), + CircleAvatar( + backgroundColor: greenPrimary, + child: Text('Test'), ), - Text( - 'Jl. Margonda Raya No.358, Kemir...', - style: TextStyle( - fontSize: 15, - color: Colors.black, - fontFamily: 'Muli', + Container( + padding: EdgeInsets.all(doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + Text( + places[index].nama, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w800, + color: Colors.black, + fontFamily: 'Muli', + ), + ), + Text( + places[index].alamat, + style: TextStyle( + fontSize: 15, + color: Colors.black, + fontFamily: 'Muli', + ), + ), + ], ), ), ], ), - ), - Icon( - Icons.arrow_forward_ios, - color: Colors.grey[400], - size: 20, - ) - ], + Icon( + Icons.arrow_forward_ios, + color: Colors.grey[400], + size: 20, + ) + ], + ), ), - ), - Container( - decoration: BoxDecoration( - border: Border(top: BorderSide(color: Colors.grey[400]))), - ), - ], - ), - ); + ); + }); } } diff --git a/lib/repository/LokasiRepository.dart b/lib/repository/LokasiRepository.dart new file mode 100644 index 00000000..04c582f4 --- /dev/null +++ b/lib/repository/LokasiRepository.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:ppl_disabilitas/model/lokasi.dart'; +import 'package:ppl_disabilitas/network/cookies_interface.dart'; +import 'package:ppl_disabilitas/network/network_interface.dart'; + +class LokasiRepository { + NetworkInterface _network = NetworkInterface(); + + Future<LokasiListResponse> fetchLokasi() async { + final response = await _network.get( + url: 'https://my.api.mockaroo.com/mall.json?key=dbcde960', + isLogin: false); + return LokasiListResponse.fromJson(response); + } + + Future<LokasiListResponse> fetchRecentSearch() async { + var response; + await CookiesInterface().checkCookieFileAvailability(fileName: "searchhistory").then((boolean) async { + if (!boolean) { + response = []; + } else { + await CookiesInterface().getCookieFile(fileName: "searchhistory").then((cookie) { + response = json.decode(cookie); + }); + } + }); + return LokasiListResponse.fromJson(response); + } + + Future<void> saveRecentSearch(Lokasi recentSearch) async { + Map<String, dynamic> searchToMap = recentSearch.toJson(); + await CookiesInterface() + .createSearchHistoryCookie(recentSearch: searchToMap); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 60b1bcba..1cb5a249 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,11 +25,11 @@ dependencies: location: ^2.5.3 flutter_plugin_android_lifecycle: ^1.0.6 flutter_polyline_points: ^0.1.0 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. + path_provider: ^1.6.5 cupertino_icons: ^0.1.2 google_maps_flutter: ^0.5.24+1 flutter_dotenv: ^2.1.0 + json_serializable: ^3.2.5 dev_dependencies: flutter_test: @@ -37,6 +37,7 @@ dev_dependencies: flutter_launcher_icons: ^0.7.4 # Linter dependency pedantic: ^1.8.0 # The default Linter package used in Google + build_runner: ^1.8.0 flutter_icons: android: "launcher_icon" diff --git a/test/cookie_test.dart b/test/cookie_test.dart new file mode 100644 index 00000000..7a35a8c5 --- /dev/null +++ b/test/cookie_test.dart @@ -0,0 +1,63 @@ +import 'dart:io'; +import 'package:flutter/services.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ppl_disabilitas/network/cookies_interface.dart'; + +class MockCookiesInterface extends Mock implements CookiesInterface {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + MethodChannel channel = + const MethodChannel('plugins.flutter.io/path_provider'); + setUpAll(() async { + // Create a temporary directory. + final directory = await Directory.systemTemp.createTemp(); + + // Mock out the MethodChannel for the path_provider plugin. + channel.setMockMethodCallHandler((MethodCall methodCall) async { + // If you're getting the apps documents directory, return the path to the + // temp directory on the test environment instead. + if (methodCall.method == 'getApplicationDocumentsDirectory') { + return directory.path; + } + return null; + }); + }); + CookiesInterface mockHttpClient; + test('Creates cookie file for sign in session', () async { + final responseHeaderFromSignIn = { + "set-cookie": + "csrftoken=v4E6UNpTMUMAoDxMoSZUBVPuAh7mkIb96DfRcakdivghb0d57yvCZxbbya7L3kFv; expires=Fri, 05 Mar 2021 03:33:39 GMT; Max-Age=31449600; Path=/; SameSite=Lax;sessionid=vrarp9pga02bwr97duemf6ym94gjgepn; expires=Fri, 20 Mar 2020 03:33:39 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax", + }; + mockHttpClient = MockCookiesInterface(); + String rootDir = + await channel.invokeMethod('getApplicationDocumentsDirectory'); + when(mockHttpClient.createSignInCookie( + responseHeaders: responseHeaderFromSignIn)) + .thenAnswer((_) async { + await Future.delayed(Duration(milliseconds: 50)); + return Future<File>.value(File("$rootDir/usercookies.json")); + }); + // combine with sign in test here + }); + test('Creates cookie file after search', () async { + final recentSearch = { + "nama": "Johnson", + "latitude": -2.9062039, + "longitude": 114.6905436, + "alamat": "2460 Comanche Crossing", + "telepon": "+62 805 612 4225" + }; + String rootDir = + await channel.invokeMethod("getApplicationDocumentsDirectory"); + when(mockHttpClient.createSearchHistoryCookie( + recentSearch: recentSearch + )) + .thenAnswer((_) async { + await Future.delayed(Duration(milliseconds: 50)); + return Future<File>.value(File("$rootDir/usercookies.json")); + }); + // combine with sign in test here + }); +} diff --git a/test/mock_test.dart b/test/mock_test.dart new file mode 100644 index 00000000..537257fa --- /dev/null +++ b/test/mock_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ppl_disabilitas/network/network_interface.dart'; +import 'package:ppl_disabilitas/page/dashboard/dashboard.dart'; +import 'package:http/http.dart' as http; +import 'package:pedantic/pedantic.dart'; + +class MockNavigatorObserver extends Mock implements NavigatorObserver {} + +class MockNetwork extends Mock implements NetworkInterface {} + +class MockHttp extends Mock implements http.Client {} + +void main() { + group('Dashboard navigation tests', () { + NavigatorObserver mockObserver; + NetworkInterface mockNetwork; + MockHttp mockHttp; + setUp(() { + mockObserver = MockNavigatorObserver(); + mockNetwork = MockNetwork(); + mockHttp = MockHttp(); + when(mockHttp.get('http://wwww.google.com')) + .thenAnswer((_) async => http.Response('{"title": "Test"}', 200)); + when(mockNetwork.get(isLogin: false, url: anyNamed('url'))) + .thenAnswer((_) async { + await Future.delayed(Duration(milliseconds: 50)); + return Future<dynamic>.value([ + { + "nama": "Coolidge", + "latitude": -23.7169139, + "longitude": -46.8498038, + "alamat": "74809 Hooker Drive", + "telepon": "+55 956 836 5799" + } + ]); + }); + }); + + Future<Null> _buildDashboardPage(WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Dashboard(), + + /// This mocked observer will now receive all navigation events + /// that happen in our app. + navigatorObservers: [mockObserver], + )); + + /// The tester.pumpWidget() call above just built our app widget + /// and triggered the pushObserver method on the mockObserver once. + verify(mockObserver.didPush(any, any)); + } + + Future<Null> _navigateToPencarianPage(WidgetTester tester) async { + final textFieldKey = Key("Text Field Mau Kemana"); + await tester.tap(find.byKey(textFieldKey)); + await tester.pump(); + } + + testWidgets( + 'when tapping text form field, should navigate to pencarian page', + (WidgetTester tester) async { + final textFieldKeyPencarian = Key("Text Field Mau Kemana"); + await _buildDashboardPage(tester); + await _navigateToPencarianPage(tester); + + verify(mockObserver.didPush(any, any)); + expect(find.byKey(textFieldKeyPencarian), findsOneWidget); + }); + + testWidgets('tapping the back button should navigate back to the dashboard', + (WidgetTester tester) async { + final backIconKey = Key("Back Icon Key"); + await _buildDashboardPage(tester); + await _navigateToPencarianPage(tester); + await tester.pump(); + final Route pushedRoute = + verify(mockObserver.didPush(captureAny, any)).captured.single; + String popResult; + unawaited(pushedRoute.popped.then((result) => popResult = result)); + await tester.tap(find.byKey(backIconKey)); + await tester.pumpAndSettle(); + expect(popResult, 'Take me back'); + }); + }); +} diff --git a/test/navigation_test.dart b/test/navigation_test.dart deleted file mode 100644 index d8952af1..00000000 --- a/test/navigation_test.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:ppl_disabilitas/page/dashboard/dashboard.dart'; -import 'package:pedantic/pedantic.dart'; - - -class MockNavigatorObserver extends Mock implements NavigatorObserver {} - -void main() { - group('Dashboard navigation tests', () { - NavigatorObserver mockObserver; - - setUp(() { - mockObserver = MockNavigatorObserver(); - }); - - Future<Null> _buildDashboardPage(WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Dashboard(), - - /// This mocked observer will now receive all navigation events - /// that happen in our app. - navigatorObservers: [mockObserver], - )); - - /// The tester.pumpWidget() call above just built our app widget - /// and triggered the pushObserver method on the mockObserver once. - verify(mockObserver.didPush(any, any)); - } - - Future<Null> _navigateToPencarianPage(WidgetTester tester) async { - final textFieldKey = Key("Text Field Mau Kemana"); - await tester.tap(find.byKey(textFieldKey)); - await tester.pumpAndSettle(); - } - - testWidgets( - 'when tapping text form field, should navigate to pencarina page', - (WidgetTester tester) async { - final textFieldKeyPencarian = Key("Text Field Mau Kemana"); - await _buildDashboardPage(tester); - await _navigateToPencarianPage(tester); - verify(mockObserver.didPush(any, any)); - expect(find.byKey(textFieldKeyPencarian), findsOneWidget); - - }); - - testWidgets('tapping the back button should navigate back to the dashboard', - (WidgetTester tester) async { - final backIconKey = Key("Back Icon Key"); - await _buildDashboardPage(tester); - await _navigateToPencarianPage(tester); - final Route pushedRoute =verify(mockObserver.didPush(captureAny, any)).captured.single; - String popResult; - unawaited(pushedRoute.popped.then((result) => popResult = result)); - await tester.tap(find.byKey(backIconKey)); - await tester.pumpAndSettle(); - expect(popResult, 'Take me back'); - - - - }); - }); -} \ No newline at end of file diff --git a/test/pencarian_test.dart b/test/pencarian_test.dart index 43546747..db66d482 100644 --- a/test/pencarian_test.dart +++ b/test/pencarian_test.dart @@ -6,19 +6,52 @@ // tree, read text, and verify that the values of widget properties are correct. +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:ppl_disabilitas/page/pencarian/pencarian.dart'; +import 'package:ppl_disabilitas/network/network_interface.dart'; +class MockNetwork extends Mock implements NetworkInterface {} void main() { + MockNetwork mockNetwork; + setUp(() { + mockNetwork = MockNetwork(); + when(mockNetwork.get(isLogin: false, url: anyNamed('url'))).thenAnswer((_) async { + await Future.delayed(Duration(milliseconds: 50)); + return Future<dynamic>.value( + [ + { + "nama": "Coolidge", + "latitude": -23.7169139, + "longitude": -46.8498038, + "alamat": "74809 Hooker Drive", + "telepon": "+55 956 836 5799" + } + ] + ); + }); + }); testWidgets('display list view in pencarian', (WidgetTester tester) async { - // Provide the childWidget to the Container. await tester.pumpWidget(MaterialApp(home: Pencarian())); + expect(find.byKey(const Key("Text Field Mau Kemana")), findsOneWidget); + //var textField = find.byKey(const Key("Text Field Mau Kemana")); + //await tester.tap(textField); + await tester.enterText(find.byKey(const Key("Text Field Mau Kemana")), "Coolidge"); + await tester.pump(); + expect(find.text("Hasil Pencarian"), findsOneWidget); + + //expect(find.byType(CircleAvatar), findsWidgets); + + //expect(find.byKey(const Key("api-Coolidge")), findsOneWidget); + // [TODO] tiap item itu punya key unik // Search for the childWidget in the tree and verify it exists. - expect(find.byType(ListView), findsOneWidget); - expect(find.byType(Container), findsNWidgets(7)); - expect(find.byType(Icon), findsNWidgets(5)); + //expect(find.byType(ListView), findsNWidgets); + //expect(find.byType(Container), findsWidgets); + //expect(find.byType(Icon), findsWidgets); }); testWidgets('finds a text field in pencarian', (WidgetTester tester) async { @@ -27,4 +60,13 @@ void main() { expect(find.byKey(textFieldKey), findsOneWidget); }); + + testWidgets('test textfield result', (WidgetTester tester) async { + final textFieldKey = Key("Text Field Mau Kemana"); + await tester.pumpWidget(MaterialApp(home: Pencarian())); + await tester.enterText(find.byKey(textFieldKey), 'Mallory'); + await tester.pump(); + expect(find.text('Mallory'), findsOneWidget); +}); + } diff --git a/test/widget_test.dart b/test/widget_test.dart index 0337d309..0a223972 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -14,7 +14,7 @@ void main() { await tester.pumpWidget(MaterialApp(home: Dashboard())); // Search for the childWidget in the tree and verify it exists. expect(find.byType(Scaffold), findsOneWidget); - expect(find.byType(Stack), findsNWidgets(2)); + expect(find.byKey(Key("Stack")), findsOneWidget); expect(find.byType(TextFormField), findsOneWidget); expect(find.byType(Icon), findsNWidgets(3)); expect(find.text('Kamu mau kemana?'), findsOneWidget); -- GitLab