From b337ee72bafae1775c6b0644db242cee7ea32aab Mon Sep 17 00:00:00 2001 From: slava-r-epam Date: Tue, 9 Jul 2024 22:41:32 +0400 Subject: [PATCH 1/5] test: Add WebView mocks and tests --- .../lib/src/ada_web_view.dart | 18 +- .../lib/src/customized_web_view.dart | 15 +- packages/ada_chat_flutter/pubspec.yaml | 1 + .../test/src/ada_web_view_test.dart | 125 ++++++++++ .../test/src/customized_web_view_test.dart | 163 +++++++++++++ .../ada_chat_flutter/test/webview_mocks.dart | 215 ++++++++++++++++++ 6 files changed, 526 insertions(+), 11 deletions(-) create mode 100644 packages/ada_chat_flutter/test/src/ada_web_view_test.dart create mode 100644 packages/ada_chat_flutter/test/src/customized_web_view_test.dart create mode 100644 packages/ada_chat_flutter/test/webview_mocks.dart diff --git a/packages/ada_chat_flutter/lib/src/ada_web_view.dart b/packages/ada_chat_flutter/lib/src/ada_web_view.dart index 33d8ec37..b5c637cf 100644 --- a/packages/ada_chat_flutter/lib/src/ada_web_view.dart +++ b/packages/ada_chat_flutter/lib/src/ada_web_view.dart @@ -100,8 +100,9 @@ class AdaWebView extends StatefulWidget { final void Function(String request, String response)? onLoadingError; @override - State createState() => _AdaWebViewState(); + State createState() => AdaWebViewState(); + // coverage:ignore-start @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -135,9 +136,11 @@ class AdaWebView extends StatefulWidget { properties.add(DoubleProperty('rolloutOverride', rolloutOverride)); properties.add(DiagnosticsProperty('testMode', testMode)); } +// coverage:ignore-end } -class _AdaWebViewState extends State { +@visibleForTesting +class AdaWebViewState extends State { late final WebViewController _controller; @override @@ -175,7 +178,7 @@ class _AdaWebViewState extends State { onUrlChange: _onUrlChange, onProgress: _onProgress, onPageStarted: _onPageStarted, - onPageFinished: _onPageFinished, + onPageFinished: onPageFinished, onHttpError: _onHttpError, onWebResourceError: _onWebResourceError, onNavigationRequest: _onNavigationRequest, @@ -193,7 +196,7 @@ class _AdaWebViewState extends State { log('AdaWebView:onNavigationRequest: ' 'url=${uri.toString()}, isMainFrame=${request.isMainFrame}'); - if (_isAdaChatLink(uri) || _isAdaSupportLink(uri) || _isBlankPage(uri)) { + if (isInternalAdaUrl(uri)) { return NavigationDecision.navigate; } @@ -212,6 +215,10 @@ class _AdaWebViewState extends State { return NavigationDecision.prevent; } + @visibleForTesting + bool isInternalAdaUrl(Uri uri) => + _isAdaChatLink(uri) || _isAdaSupportLink(uri) || _isBlankPage(uri); + bool _isBlankPage(Uri uri) => uri.toString() == 'about:blank'; bool _isAdaSupportLink(Uri uri) => uri.host == '${widget.handle}.ada.support'; @@ -230,7 +237,8 @@ class _AdaWebViewState extends State { 'headers=${error.response?.headers}, ', ); - void _onPageFinished(String url) { + @visibleForTesting + void onPageFinished(String url) { log('AdaWebView:onPageFinished: url=$url'); Future.delayed(Duration.zero, () async { diff --git a/packages/ada_chat_flutter/lib/src/customized_web_view.dart b/packages/ada_chat_flutter/lib/src/customized_web_view.dart index 76492fe8..c7aec9bb 100644 --- a/packages/ada_chat_flutter/lib/src/customized_web_view.dart +++ b/packages/ada_chat_flutter/lib/src/customized_web_view.dart @@ -16,10 +16,11 @@ class CustomizedWebView extends StatefulWidget { final BrowserSettings? browserSettings; @override - State createState() => _CustomizedWebViewState(); + State createState() => CustomizedWebViewState(); } -class _CustomizedWebViewState extends State { +@visibleForTesting +class CustomizedWebViewState extends State { late final WebViewController _webViewController = WebViewController(); late final AdaButtonHide _adaButtonHide = AdaButtonHide( webViewController: _webViewController, @@ -37,9 +38,9 @@ class _CustomizedWebViewState extends State { ..setNavigationDelegate( NavigationDelegate( onUrlChange: _onUrlChange, - onProgress: _onProgress, + onProgress: onProgress, onPageStarted: _onPageStarted, - onPageFinished: _onPageFinished, + onPageFinished: onPageFinished, onNavigationRequest: _onNavigationRequest, ), ) @@ -56,7 +57,8 @@ class _CustomizedWebViewState extends State { void _onUrlChange(change) => log('CustomizedWebView:onUrlChange: url=${change.url}'); - Future _onPageFinished(String url) async { + @visibleForTesting + Future onPageFinished(String url) async { log('CustomizedWebView:onPageFinished: url=$url'); final pageController = widget.browserSettings?.control; @@ -85,7 +87,8 @@ class _CustomizedWebViewState extends State { await _adaButtonHide.maybeHideButton(url); } - void _onProgress(int progress) { + @visibleForTesting + void onProgress(int progress) { log('CustomizedWebView:onProgress: progress=$progress'); final pageController = widget.browserSettings?.control; diff --git a/packages/ada_chat_flutter/pubspec.yaml b/packages/ada_chat_flutter/pubspec.yaml index 693f12fc..23c5bff3 100644 --- a/packages/ada_chat_flutter/pubspec.yaml +++ b/packages/ada_chat_flutter/pubspec.yaml @@ -17,6 +17,7 @@ dev_dependencies: flutter_test: sdk: flutter mocktail: ^1.0.3 + webview_flutter_platform_interface: ^2.10.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart new file mode 100644 index 00000000..658bc918 --- /dev/null +++ b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart @@ -0,0 +1,125 @@ +import 'package:ada_chat_flutter/src/ada_controller.dart'; +import 'package:ada_chat_flutter/src/ada_web_view.dart'; +import 'package:ada_chat_flutter/src/browser_settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../webview_mocks.dart'; + +class MockAdaController extends Mock implements AdaController {} + +const _title = '_title'; +const _regexp = '_regexp'; +const _handle = '_handle'; +const _embedUri = 'https://example.com/embed.html'; + +void main() { + late MockAdaController mockAdaController; + + setUp(() { + WebViewPlatform.instance = FakeWebViewPlatform(); + + mockAdaController = MockAdaController(); + + when(() => mockAdaController.start()).thenAnswer((_) async {}); + }); + + group('AdaWebView tests - ', () { + testWidgets( + 'WHEN AdaWebView is pumped ' + 'THEN should show correct widgets', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: _buildAdaWebView(mockAdaController), + ), + ); + + expect( + webViewCalls, + equals(['loadRequest: uri=$_embedUri']), + ); + }, + ); + + testWidgets( + 'GIVEN the widget is pumped ' + 'WHEN isInternalAdaUrl is called ' + 'THEN should rebuild correct result', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: _buildAdaWebView(mockAdaController), + ), + ); + + final state = tester.state(find.byType(AdaWebView)); + + expect(state.isInternalAdaUrl(Uri.parse('about:blank')), true); + expect( + state.isInternalAdaUrl(Uri.parse('https://$_handle.ada.support/asd')), + true, + ); + expect(state.isInternalAdaUrl(Uri.parse(_embedUri)), true); + + expect(state.isInternalAdaUrl(Uri.parse('google.com')), false); + }, + ); + + testWidgets( + 'GIVEN the widget is pumped ' + 'WHEN onPageFinished is called ' + 'THEN should rebuild correct result', + (WidgetTester tester) async { + await tester.runAsync(() async { + await tester.pumpWidget( + MaterialApp( + home: _buildAdaWebView(mockAdaController), + ), + ); + + final state = tester.state(find.byType(AdaWebView)); + + state.onPageFinished(_embedUri); + await Future.delayed(Duration.zero); + + expect( + webViewCalls, + equals(['loadRequest: uri=$_embedUri']), + ); + verify(() => mockAdaController.start()).called(1); + // expect(state.isInternalAdaUrl(Uri.parse('google.com')), false); + }); + }, + ); + }); +} + +Widget _buildAdaWebView(MockAdaController mockAdaController) { + return AdaWebView( + embedUri: Uri.parse(_embedUri), + handle: _handle, + controller: mockAdaController, + rolloutOverride: 1, + language: 'en', + metaFields: const {'key': 'value'}, + browserSettings: BrowserSettings( + pageBuilder: ( + context, + browser, + controller, + ) => + Column( + children: [ + Text(_title), + browser, + ], + ), + adaHideUrls: [ + RegExp(_regexp), + ], + ), + ); +} diff --git a/packages/ada_chat_flutter/test/src/customized_web_view_test.dart b/packages/ada_chat_flutter/test/src/customized_web_view_test.dart new file mode 100644 index 00000000..2e263e15 --- /dev/null +++ b/packages/ada_chat_flutter/test/src/customized_web_view_test.dart @@ -0,0 +1,163 @@ +import 'package:ada_chat_flutter/src/browser_settings.dart'; +import 'package:ada_chat_flutter/src/customized_web_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../webview_mocks.dart'; + +const _uri = 'https://example.com/page.html'; +const _title = '_title'; +const _regexp = '_regexp'; + +const _keyLeft = Key('left'); +const _keyRight = Key('right'); +const _keyReload = Key('reload'); + +void main() { + setUp(() { + WebViewPlatform.instance = FakeWebViewPlatform(); + }); + + group('CustomizedWebView tests - ', () { + testWidgets( + 'WHEN the widget is pumped ' + 'THEN should load the provided URL', + (WidgetTester tester) async { + await _pumpWidget(tester); + + expect( + webViewCalls, + equals(['loadRequest: uri=$_uri']), + ); + }, + ); + + testWidgets( + 'WHEN the widget is pumped ' + 'THEN should contain correct widgets', + (WidgetTester tester) async { + await _pumpWidget(tester); + + expect(find.byType(WebViewWidget), findsOneWidget); + expect(find.text(_title), findsOneWidget); + expect(find.byKey(_keyLeft), findsOneWidget); + expect(find.byKey(_keyRight), findsOneWidget); + expect(find.byKey(_keyReload), findsOneWidget); + }, + ); + + testWidgets( + 'GIVEN the widget is pumped ' + 'WHEN page controller methods called ' + 'THEN should call correct webview controller methods', + (WidgetTester tester) async { + await _pumpWidget(tester); + + await _initAndGetWidgetState(tester); + + await tester.tap(find.byKey(_keyLeft)); + await tester.pumpAndSettle(); + + expect( + webViewCalls, + equals(['loadRequest: uri=$_uri', 'goBack']), + ); + + await tester.tap(find.byKey(_keyRight)); + await tester.pumpAndSettle(); + + expect( + webViewCalls, + equals(['loadRequest: uri=$_uri', 'goBack', 'goForward']), + ); + + await tester.tap(find.byKey(_keyReload)); + await tester.pumpAndSettle(); + + expect( + webViewCalls, + equals(['loadRequest: uri=$_uri', 'goBack', 'goForward', 'reload']), + ); + }, + ); + + testWidgets( + 'GIVEN the widget is pumped ' + 'WHEN progress is changed ' + 'THEN should rebuild screen with updated value', + (WidgetTester tester) async { + await _pumpWidget(tester); + + final state = await _initAndGetWidgetState(tester); + + state.onProgress(50); + await tester.pumpAndSettle(); + + expect(find.text('50 %'), findsOneWidget); + + state.onProgress(100); + await tester.pumpAndSettle(); + + expect(find.text('100 %'), findsOneWidget); + }, + ); + }); +} + +Future _initAndGetWidgetState( + WidgetTester tester) async { + final state = + tester.state(find.byType(CustomizedWebView)); + await state.onPageFinished(_uri); + return state; +} + +Future _pumpWidget(WidgetTester tester) => tester.pumpWidget( + MaterialApp( + home: CustomizedWebView( + url: Uri.parse(_uri), + browserSettings: BrowserSettings( + pageBuilder: ( + context, + browser, + controller, + ) => + Column( + children: [ + Row( + children: [ + IconButton( + key: _keyLeft, + icon: Icon(Icons.keyboard_arrow_left), + onPressed: () => controller.goBack(), + ), + IconButton( + key: _keyRight, + icon: Icon(Icons.keyboard_arrow_right), + onPressed: () => controller.goForward(), + ), + Expanded(child: Text(_title)), + IconButton( + key: _keyReload, + icon: Icon(Icons.refresh), + onPressed: () => controller.reload(), + ), + ListenableBuilder( + listenable: controller, + builder: (context, _) { + return Text('${controller.progress} %'); + }, + ), + ], + ), + browser, + ], + ), + adaHideUrls: [ + RegExp(_regexp), + ], + ), + ), + ), + ); diff --git a/packages/ada_chat_flutter/test/webview_mocks.dart b/packages/ada_chat_flutter/test/webview_mocks.dart new file mode 100644 index 00000000..897ccb6c --- /dev/null +++ b/packages/ada_chat_flutter/test/webview_mocks.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart'; +import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart'; +import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart'; +import 'package:webview_flutter_platform_interface/src/types/load_request_params.dart'; + +final webViewCalls = []; + +class FakePlatformWebViewCookieManager extends PlatformWebViewCookieManager { + FakePlatformWebViewCookieManager( + super.params, + ) : super.implementation(); + + @override + Future clearCookies() async { + return false; + } + + @override + Future setCookie(WebViewCookie cookie) async {} +} + +class FakeWebViewController extends PlatformWebViewController { + FakeWebViewController(super.params) : super.implementation(); + + @override + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams, + ) async {} + + @override + Future canGoBack() async { + return true; + } + + @override + Future canGoForward() async { + return true; + } + + @override + Future clearCache() async {} + + @override + Future clearLocalStorage() async {} + + @override + Future currentUrl() async { + return 'https://example.com/page.html'; + } + + @override + Future enableZoom(bool enabled) async {} + + @override + Future getScrollPosition() async { + return Offset(0, 0); + } + + @override + Future getTitle() async { + return null; + } + + @override + Future getUserAgent() async { + return null; + } + + @override + Future goBack() async => webViewCalls.add('goBack'); + + @override + Future goForward() async => webViewCalls.add('goForward'); + + @override + Future loadFile(String absoluteFilePath) async {} + + @override + Future loadFlutterAsset(String key) async {} + + @override + Future loadHtmlString(String html, {String? baseUrl}) async {} + + @override + Future loadRequest(LoadRequestParams params) async { + webViewCalls.add('loadRequest: uri=${params.uri}'); + } + + @override + PlatformWebViewControllerCreationParams get params => + PlatformWebViewControllerCreationParams(); + + @override + Future reload() async => webViewCalls.add('reload'); + + @override + Future removeJavaScriptChannel(String javaScriptChannelName) async {} + + @override + Future runJavaScript(String javaScript) async {} + + @override + Future runJavaScriptReturningResult(String javaScript) async { + return {}; + } + + @override + Future scrollBy(int x, int y) async {} + + @override + Future scrollTo(int x, int y) async {} + + @override + Future setBackgroundColor(Color color) async {} + + @override + Future setJavaScriptMode(JavaScriptMode javaScriptMode) async {} + + @override + Future setOnConsoleMessage( + void Function(JavaScriptConsoleMessage consoleMessage) onConsoleMessage, + ) async {} + + @override + Future setOnPlatformPermissionRequest( + void Function(PlatformWebViewPermissionRequest request) onPermissionRequest, + ) async {} + + @override + Future setPlatformNavigationDelegate( + PlatformNavigationDelegate handler, + ) async {} + + @override + Future setUserAgent(String? userAgent) async {} +} + +class FakeWebViewWidget extends PlatformWebViewWidget { + FakeWebViewWidget(super.params) : super.implementation(); + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class FakePlatformNavigationDelegate extends PlatformNavigationDelegate { + FakePlatformNavigationDelegate(super.params) : super.implementation(); + + @override + Future setOnPageStarted(PageEventCallback onPageStarted) async {} + + @override + Future setOnNavigationRequest( + NavigationRequestCallback onNavigationRequest, + ) async {} + + @override + Future setOnPageFinished(PageEventCallback onPageFinished) async {} + + @override + Future setOnProgress(ProgressCallback onProgress) async {} + + @override + Future setOnWebResourceError( + WebResourceErrorCallback onWebResourceError) async {} + + @override + Future setOnUrlChange(UrlChangeCallback onUrlChange) async {} + + @override + Future setOnHttpAuthRequest( + HttpAuthRequestCallback onHttpAuthRequest) async {} + + @override + Future setOnHttpError(HttpResponseErrorCallback onHttpError) async {} +} + +class FakeWebViewPlatform extends WebViewPlatform { + FakeWebViewPlatform() : super() { + webViewCalls.clear(); + } + + @override + PlatformWebViewCookieManager createPlatformCookieManager( + PlatformWebViewCookieManagerCreationParams params, + ) { + return FakePlatformWebViewCookieManager(params); + } + + @override + PlatformNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + return FakePlatformNavigationDelegate(params); + } + + @override + PlatformWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return FakeWebViewWidget(params); + } + + @override + PlatformWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + return FakeWebViewController(params); + } +} From af11881afa2be95d94b5eb78be1dc9b5989e51ba Mon Sep 17 00:00:00 2001 From: slava-r-epam Date: Tue, 9 Jul 2024 23:08:51 +0400 Subject: [PATCH 2/5] test: Add WebView mocks and tests --- .../test/src/ada_web_view_test.dart | 22 +++++++++++++++---- .../ada_chat_flutter/test/webview_mocks.dart | 14 +++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart index 658bc918..bac968b3 100644 --- a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart +++ b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart @@ -71,7 +71,7 @@ void main() { testWidgets( 'GIVEN the widget is pumped ' 'WHEN onPageFinished is called ' - 'THEN should rebuild correct result', + 'THEN should init and start Ada with correct params', (WidgetTester tester) async { await tester.runAsync(() async { await tester.pumpWidget( @@ -85,12 +85,26 @@ void main() { state.onPageFinished(_embedUri); await Future.delayed(Duration.zero); + verify(() => mockAdaController.start()).called(1); expect( webViewCalls, - equals(['loadRequest: uri=$_embedUri']), + contains('loadRequest: uri=https://example.com/embed.html'), + ); + expect( + webViewCalls, + contains('addJavaScriptChannel: name=onLoaded'), + ); + expect( + webViewCalls, + anyElement(contains('"metaFields":{"key":"value"')), + ); + expect( + webViewCalls, + anyElement( + contains('{"handle":"$_handle","language":"en","cluster":' + 'null,"domain":null,"hideMask":false'), + ), ); - verify(() => mockAdaController.start()).called(1); - // expect(state.isInternalAdaUrl(Uri.parse('google.com')), false); }); }, ); diff --git a/packages/ada_chat_flutter/test/webview_mocks.dart b/packages/ada_chat_flutter/test/webview_mocks.dart index 897ccb6c..77ee1341 100644 --- a/packages/ada_chat_flutter/test/webview_mocks.dart +++ b/packages/ada_chat_flutter/test/webview_mocks.dart @@ -29,7 +29,10 @@ class FakeWebViewController extends PlatformWebViewController { @override Future addJavaScriptChannel( JavaScriptChannelParams javaScriptChannelParams, - ) async {} + ) async => + webViewCalls.add( + 'addJavaScriptChannel: name=${javaScriptChannelParams.name}', + ); @override Future canGoBack() async { @@ -101,11 +104,16 @@ class FakeWebViewController extends PlatformWebViewController { Future removeJavaScriptChannel(String javaScriptChannelName) async {} @override - Future runJavaScript(String javaScript) async {} + Future runJavaScript(String javaScript) async => webViewCalls.add( + 'runJavaScript: javaScript=$javaScript', + ); @override Future runJavaScriptReturningResult(String javaScript) async { - return {}; + webViewCalls.add( + 'runJavaScriptReturningResult: javaScript=$javaScript', + ); + return '{}'; } @override From 3cc2b143ee8b3294ac0e0484576c0e45e9797bde Mon Sep 17 00:00:00 2001 From: slava-r-epam Date: Tue, 9 Jul 2024 23:12:49 +0400 Subject: [PATCH 3/5] refactor: Small refactor --- .../test/src/ada_web_view_test.dart | 8 ++-- .../ada_chat_flutter/test/webview_mocks.dart | 38 +++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart index bac968b3..d01d733c 100644 --- a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart +++ b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart @@ -8,7 +8,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import '../webview_mocks.dart'; -class MockAdaController extends Mock implements AdaController {} +class _MockAdaController extends Mock implements AdaController {} const _title = '_title'; const _regexp = '_regexp'; @@ -16,12 +16,12 @@ const _handle = '_handle'; const _embedUri = 'https://example.com/embed.html'; void main() { - late MockAdaController mockAdaController; + late _MockAdaController mockAdaController; setUp(() { WebViewPlatform.instance = FakeWebViewPlatform(); - mockAdaController = MockAdaController(); + mockAdaController = _MockAdaController(); when(() => mockAdaController.start()).thenAnswer((_) async {}); }); @@ -111,7 +111,7 @@ void main() { }); } -Widget _buildAdaWebView(MockAdaController mockAdaController) { +Widget _buildAdaWebView(_MockAdaController mockAdaController) { return AdaWebView( embedUri: Uri.parse(_embedUri), handle: _handle, diff --git a/packages/ada_chat_flutter/test/webview_mocks.dart b/packages/ada_chat_flutter/test/webview_mocks.dart index 77ee1341..5ba64e0b 100644 --- a/packages/ada_chat_flutter/test/webview_mocks.dart +++ b/packages/ada_chat_flutter/test/webview_mocks.dart @@ -9,8 +9,8 @@ import 'package:webview_flutter_platform_interface/src/types/load_request_params final webViewCalls = []; -class FakePlatformWebViewCookieManager extends PlatformWebViewCookieManager { - FakePlatformWebViewCookieManager( +class _FakePlatformWebViewCookieManager extends PlatformWebViewCookieManager { + _FakePlatformWebViewCookieManager( super.params, ) : super.implementation(); @@ -23,16 +23,15 @@ class FakePlatformWebViewCookieManager extends PlatformWebViewCookieManager { Future setCookie(WebViewCookie cookie) async {} } -class FakeWebViewController extends PlatformWebViewController { - FakeWebViewController(super.params) : super.implementation(); +class _FakeWebViewController extends PlatformWebViewController { + _FakeWebViewController(super.params) : super.implementation(); @override Future addJavaScriptChannel( JavaScriptChannelParams javaScriptChannelParams, ) async => - webViewCalls.add( - 'addJavaScriptChannel: name=${javaScriptChannelParams.name}', - ); + webViewCalls.add('addJavaScriptChannel: ' + 'name=${javaScriptChannelParams.name}'); @override Future canGoBack() async { @@ -89,9 +88,8 @@ class FakeWebViewController extends PlatformWebViewController { Future loadHtmlString(String html, {String? baseUrl}) async {} @override - Future loadRequest(LoadRequestParams params) async { - webViewCalls.add('loadRequest: uri=${params.uri}'); - } + Future loadRequest(LoadRequestParams params) async => + webViewCalls.add('loadRequest: uri=${params.uri}'); @override PlatformWebViewControllerCreationParams get params => @@ -110,9 +108,7 @@ class FakeWebViewController extends PlatformWebViewController { @override Future runJavaScriptReturningResult(String javaScript) async { - webViewCalls.add( - 'runJavaScriptReturningResult: javaScript=$javaScript', - ); + webViewCalls.add('runJavaScriptReturningResult: javaScript=$javaScript'); return '{}'; } @@ -147,8 +143,8 @@ class FakeWebViewController extends PlatformWebViewController { Future setUserAgent(String? userAgent) async {} } -class FakeWebViewWidget extends PlatformWebViewWidget { - FakeWebViewWidget(super.params) : super.implementation(); +class _FakeWebViewWidget extends PlatformWebViewWidget { + _FakeWebViewWidget(super.params) : super.implementation(); @override Widget build(BuildContext context) { @@ -156,8 +152,8 @@ class FakeWebViewWidget extends PlatformWebViewWidget { } } -class FakePlatformNavigationDelegate extends PlatformNavigationDelegate { - FakePlatformNavigationDelegate(super.params) : super.implementation(); +class _FakePlatformNavigationDelegate extends PlatformNavigationDelegate { + _FakePlatformNavigationDelegate(super.params) : super.implementation(); @override Future setOnPageStarted(PageEventCallback onPageStarted) async {} @@ -197,27 +193,27 @@ class FakeWebViewPlatform extends WebViewPlatform { PlatformWebViewCookieManager createPlatformCookieManager( PlatformWebViewCookieManagerCreationParams params, ) { - return FakePlatformWebViewCookieManager(params); + return _FakePlatformWebViewCookieManager(params); } @override PlatformNavigationDelegate createPlatformNavigationDelegate( PlatformNavigationDelegateCreationParams params, ) { - return FakePlatformNavigationDelegate(params); + return _FakePlatformNavigationDelegate(params); } @override PlatformWebViewWidget createPlatformWebViewWidget( PlatformWebViewWidgetCreationParams params, ) { - return FakeWebViewWidget(params); + return _FakeWebViewWidget(params); } @override PlatformWebViewController createPlatformWebViewController( PlatformWebViewControllerCreationParams params, ) { - return FakeWebViewController(params); + return _FakeWebViewController(params); } } From 3c2a76cbfc63d1378c5e1498b9767f9c1a9293a4 Mon Sep 17 00:00:00 2001 From: slava-r-epam Date: Thu, 11 Jul 2024 17:36:51 +0400 Subject: [PATCH 4/5] test: Add some tests --- .../lib/src/ada_web_view.dart | 29 +----- packages/ada_chat_flutter/lib/src/utils.dart | 22 +++++ .../test/src/ada_web_view_test.dart | 91 +++++++++++-------- .../ada_chat_flutter/test/src/utils_test.dart | 77 ++++++++++++++++ 4 files changed, 155 insertions(+), 64 deletions(-) diff --git a/packages/ada_chat_flutter/lib/src/ada_web_view.dart b/packages/ada_chat_flutter/lib/src/ada_web_view.dart index b5c637cf..5bb48e1d 100644 --- a/packages/ada_chat_flutter/lib/src/ada_web_view.dart +++ b/packages/ada_chat_flutter/lib/src/ada_web_view.dart @@ -181,7 +181,7 @@ class AdaWebViewState extends State { onPageFinished: onPageFinished, onHttpError: _onHttpError, onWebResourceError: _onWebResourceError, - onNavigationRequest: _onNavigationRequest, + onNavigationRequest: onNavigationRequest, ), ); @@ -191,12 +191,13 @@ class AdaWebViewState extends State { void _onConsoleMessage(JavaScriptConsoleMessage message) => widget.onConsoleMessage?.call(message.level.toString(), message.message); - FutureOr _onNavigationRequest(NavigationRequest request) { + @visibleForTesting + FutureOr onNavigationRequest(NavigationRequest request) { final uri = Uri.parse(request.url); log('AdaWebView:onNavigationRequest: ' 'url=${uri.toString()}, isMainFrame=${request.isMainFrame}'); - if (isInternalAdaUrl(uri)) { + if (isInternalAdaUrl(uri, widget.embedUri, widget.handle)) { return NavigationDecision.navigate; } @@ -215,16 +216,6 @@ class AdaWebViewState extends State { return NavigationDecision.prevent; } - @visibleForTesting - bool isInternalAdaUrl(Uri uri) => - _isAdaChatLink(uri) || _isAdaSupportLink(uri) || _isBlankPage(uri); - - bool _isBlankPage(Uri uri) => uri.toString() == 'about:blank'; - - bool _isAdaSupportLink(Uri uri) => uri.host == '${widget.handle}.ada.support'; - - bool _isAdaChatLink(Uri uri) => uri == widget.embedUri; - void _onWebResourceError(WebResourceError error) => log('AdaWebView:onWebResourceError: ' 'errorCode=${error.errorCode}, ' @@ -232,9 +223,7 @@ class AdaWebViewState extends State { void _onHttpError(HttpResponseError error) => widget.onLoadingError?.call( error.request?.uri.toString() ?? '', - 'uri=${error.response?.uri}, ' - 'statusCode=${error.response?.statusCode}, ' - 'headers=${error.response?.headers}, ', + 'statusCode=${error.response?.statusCode}', ); @visibleForTesting @@ -383,12 +372,4 @@ console.log("adaSettings: " + JSON.stringify(window.adaSettings)); }, ); } - - dynamic jsonStrToMap(String message) { - if (message.isEmpty) { - return {}; - } - - return jsonDecode(message); - } } diff --git a/packages/ada_chat_flutter/lib/src/utils.dart b/packages/ada_chat_flutter/lib/src/utils.dart index c0b8197e..027e8108 100644 --- a/packages/ada_chat_flutter/lib/src/utils.dart +++ b/packages/ada_chat_flutter/lib/src/utils.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; String get getOsName { @@ -9,3 +11,23 @@ String get getOsName { return 'N/A'; } } + +bool isInternalAdaUrl(Uri uri, Uri embedUri, String handle) => + isAdaChatLink(uri, embedUri) || + isAdaSupportLink(uri, handle) || + isBlankPage(uri); + +bool isBlankPage(Uri uri) => uri.toString() == 'about:blank'; + +bool isAdaSupportLink(Uri uri, String handle) => + uri.host == '$handle.ada.support'; + +bool isAdaChatLink(Uri uri, Uri embedUri) => uri == embedUri; + +dynamic jsonStrToMap(String message) { + if (message.isEmpty) { + return {}; + } + + return json.decode(message); +} diff --git a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart index d01d733c..cbb693f0 100644 --- a/packages/ada_chat_flutter/test/src/ada_web_view_test.dart +++ b/packages/ada_chat_flutter/test/src/ada_web_view_test.dart @@ -1,6 +1,7 @@ import 'package:ada_chat_flutter/src/ada_controller.dart'; import 'package:ada_chat_flutter/src/ada_web_view.dart'; import 'package:ada_chat_flutter/src/browser_settings.dart'; +import 'package:ada_chat_flutter/src/customized_web_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -15,6 +16,23 @@ const _regexp = '_regexp'; const _handle = '_handle'; const _embedUri = 'https://example.com/embed.html'; +final _browserSettings = BrowserSettings( + pageBuilder: ( + context, + browser, + controller, + ) => + Column( + children: [ + Text(_title), + browser, + ], + ), + adaHideUrls: [ + RegExp(_regexp), + ], +); + void main() { late _MockAdaController mockAdaController; @@ -44,30 +62,6 @@ void main() { }, ); - testWidgets( - 'GIVEN the widget is pumped ' - 'WHEN isInternalAdaUrl is called ' - 'THEN should rebuild correct result', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: _buildAdaWebView(mockAdaController), - ), - ); - - final state = tester.state(find.byType(AdaWebView)); - - expect(state.isInternalAdaUrl(Uri.parse('about:blank')), true); - expect( - state.isInternalAdaUrl(Uri.parse('https://$_handle.ada.support/asd')), - true, - ); - expect(state.isInternalAdaUrl(Uri.parse(_embedUri)), true); - - expect(state.isInternalAdaUrl(Uri.parse('google.com')), false); - }, - ); - testWidgets( 'GIVEN the widget is pumped ' 'WHEN onPageFinished is called ' @@ -108,6 +102,38 @@ void main() { }); }, ); + + testWidgets( + 'GIVEN the widget is pumped ' + 'WHEN onNavigationRequest is called for external page ' + 'THEN should show customized webview', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: _buildAdaWebView(mockAdaController), + ), + ); + + final state = tester.state(find.byType(AdaWebView)); + + state.onNavigationRequest( + NavigationRequest( + url: 'https://external-page.com/index.html', + isMainFrame: true, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text(_title), findsOneWidget); + expect( + find.byWidgetPredicate( + (w) => + w is CustomizedWebView && w.browserSettings == _browserSettings, + ), + findsOneWidget, + ); + }, + ); }); } @@ -119,21 +145,6 @@ Widget _buildAdaWebView(_MockAdaController mockAdaController) { rolloutOverride: 1, language: 'en', metaFields: const {'key': 'value'}, - browserSettings: BrowserSettings( - pageBuilder: ( - context, - browser, - controller, - ) => - Column( - children: [ - Text(_title), - browser, - ], - ), - adaHideUrls: [ - RegExp(_regexp), - ], - ), + browserSettings: _browserSettings, ); } diff --git a/packages/ada_chat_flutter/test/src/utils_test.dart b/packages/ada_chat_flutter/test/src/utils_test.dart index 248fa672..dc0014f3 100644 --- a/packages/ada_chat_flutter/test/src/utils_test.dart +++ b/packages/ada_chat_flutter/test/src/utils_test.dart @@ -31,5 +31,82 @@ void main() { expect(getOsName, equals('N/A')); }); }); + + group('isBlankPage tests - ', () { + test('isBlankPage returns true for about:blank', () { + expect(isBlankPage(Uri.parse('about:blank')), isTrue); + }); + + test('isBlankPage returns false for other URLs', () { + expect(isBlankPage(Uri.parse('https://example.com')), isFalse); + }); + }); + + group('isAdaSupportLink tests - ', () { + const handle = 'test-handle'; + + test('isAdaSupportLink returns true for valid Ada support links', () { + expect( + isAdaSupportLink( + Uri.parse('https://test-handle.ada.support'), handle), + isTrue); + }); + + test('isAdaSupportLink returns false for invalid Ada support links', () { + expect(isAdaSupportLink(Uri.parse('https://example.com'), handle), + isFalse); + }); + }); + + group('isAdaChatLink tests - ', () { + final embedUri = Uri.parse('https://example.com/embed.html'); + + test('isAdaChatLink returns true for matching embed URIs', () { + expect(isAdaChatLink(embedUri, embedUri), isTrue); + }); + + test('isAdaChatLink returns false for non-matching embed URIs', () { + expect( + isAdaChatLink(Uri.parse('https://example.com'), embedUri), isFalse); + }); + }); + + group('isInternalAdaUrl tests - ', () { + final embedUri = Uri.parse('https://example.com/embed.html'); + const handle = 'test-handle'; + + test('isInternalAdaUrl returns true for valid internal Ada URLs', () { + expect(isInternalAdaUrl(embedUri, embedUri, handle), isTrue); + expect( + isInternalAdaUrl( + Uri.parse('https://test-handle.ada.support'), embedUri, handle), + isTrue); + expect(isInternalAdaUrl(Uri.parse('about:blank'), embedUri, handle), + isTrue); + }); + + test('isInternalAdaUrl returns false for invalid internal Ada URLs', () { + expect( + isInternalAdaUrl( + Uri.parse('https://example.com'), embedUri, handle), + isFalse); + }); + }); + + group('jsonStrToMap', () { + test('returns empty map for empty string', () { + expect(jsonStrToMap(''), isEmpty); + expect(jsonStrToMap(''), isA>()); + }); + + test('decodes valid JSON string to map', () { + const jsonString = '{"key1": "value1", "key2": 123}'; + final result = jsonStrToMap(jsonString); + + expect(result, isA>()); + expect(result['key1'], equals('value1')); + expect(result['key2'], equals(123)); + }); + }); }); } From 07f1f438441fcd83043839b10d2527b95005dfcf Mon Sep 17 00:00:00 2001 From: slava-r-epam Date: Thu, 11 Jul 2024 17:51:17 +0400 Subject: [PATCH 5/5] ci: Upgrade version. Update CHANGELOG.md --- packages/ada_chat_flutter/CHANGELOG.md | 4 ++++ packages/ada_chat_flutter/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ada_chat_flutter/CHANGELOG.md b/packages/ada_chat_flutter/CHANGELOG.md index de5e9a0f..575f8dfb 100644 --- a/packages/ada_chat_flutter/CHANGELOG.md +++ b/packages/ada_chat_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.1 + +- Refactor. Add more tests. + ## 1.1.0 - Block Ada chat button on selected pages. diff --git a/packages/ada_chat_flutter/pubspec.yaml b/packages/ada_chat_flutter/pubspec.yaml index 23c5bff3..a05c50e5 100644 --- a/packages/ada_chat_flutter/pubspec.yaml +++ b/packages/ada_chat_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: ada_chat_flutter description: Flutter implementation of Ada chat -version: 1.1.0 +version: 1.1.1 environment: sdk: '>=3.4.3 <4.0.0'