diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 2343ecba1..3f7a2fc68 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,6 +1,10 @@ -## NEXT +## 0.5.0 * Code refactoring. +* Update the example app and integration_test. +* Sync with the latest framework code. +* Migrate to new analysis options. +* Update LWE binary (f0ca15ee41d2fc96b59fd57b63b6c32cf6c1906b). ## 0.4.4 diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index 59613676a..0d83a0400 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -24,8 +24,8 @@ This package is not an _endorsed_ implementation of `webview_flutter`. Therefore ```yaml dependencies: - webview_flutter: ^3.0.2 - webview_flutter_tizen: ^0.4.4 + webview_flutter: ^3.0.4 + webview_flutter_tizen: ^0.5.0 ``` ## Example diff --git a/packages/webview_flutter/analysis_options.yaml b/packages/webview_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e15..000000000 --- a/packages/webview_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 5ff2d5533..73827ce66 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -2,1395 +2,639 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math'; -import 'dart:typed_data'; +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. -import 'package:flutter/services.dart'; -import 'package:flutter/src/foundation/basic_types.dart'; -import 'package:flutter/src/gestures/recognizer.dart'; -import 'package:flutter/widgets.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_tizen/webview_flutter_tizen.dart'; - -typedef void VoidCallback(); - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final _FakePlatformViewsController fakePlatformViewsController = - _FakePlatformViewsController(); - - final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); - - setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); - SystemChannels.platform - .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); - }); - - setUp(() { - WebView.platform = TizenWebView(); - fakePlatformViewsController.reset(); - _fakeCookieManager.reset(); - }); - - testWidgets('Create WebView', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - }); - - testWidgets('Initial url', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(await controller.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Javascript mode', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); - - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.disabled, - )); - expect(platformWebView.javascriptMode, JavascriptMode.disabled); - }); - - testWidgets('Load url', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - }); - - testWidgets('Invalid urls', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(await controller!.currentUrl(), isNull); - - expect(() => controller!.loadUrl(''), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); - - // Missing schema. - expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); - }); - - testWidgets('Headers in loadUrl', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final Map headers = { - 'CACHE-CONTROL': 'ABC' - }; - await controller!.loadUrl('https://flutter.io', headers: headers); - expect(await controller!.currentUrl(), equals('https://flutter.io')); - }); - - testWidgets("Can't go back before loading a page", - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoBackNoPageLoaded = await controller!.canGoBack(); - - expect(canGoBackNoPageLoaded, false); - }); - - testWidgets("Clear Cache", (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, true); - - await controller!.clearCache(); - - expect(fakePlatformViewsController.lastCreatedView!.hasCache, false); - }); - - testWidgets("Can't go back with no history", (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - final bool canGoBackFirstPageLoaded = await controller!.canGoBack(); - - expect(canGoBackFirstPageLoaded, false); - }); - - testWidgets('Can go back', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadUrl('https://www.google.com'); - final bool canGoBackSecondPageLoaded = await controller!.canGoBack(); - - expect(canGoBackSecondPageLoaded, true); - }); - - testWidgets("Can't go forward before loading a page", - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoForwardNoPageLoaded = await controller!.canGoForward(); - - expect(canGoForwardNoPageLoaded, false); - }); - - testWidgets("Can't go forward with no history", (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - final bool canGoForwardFirstPageLoaded = await controller!.canGoForward(); - - expect(canGoForwardFirstPageLoaded, false); - }); - - testWidgets('Can go forward', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadUrl('https://youtube.com'); - await controller!.goBack(); - final bool canGoForwardFirstPageBacked = await controller!.canGoForward(); - - expect(canGoForwardFirstPageBacked, true); - }); - - testWidgets('Go back', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Go forward', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.goForward(); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - }); - - testWidgets('Current URL', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - // Test a WebView without an explicitly set first URL. - expect(await controller!.currentUrl(), isNull); - - await controller!.loadUrl('https://youtube.com'); - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); - expect(await controller!.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Reload url', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - - await controller.reload(); - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); - - await controller.loadUrl('https://youtube.com'); - - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - }); - -// All these TCs will be replaced by other TCs. -/* - testWidgets('evaluate Javascript', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - // ignore: deprecated_member_use_from_same_package - await controller.evaluateJavascript("fake js string"), - "fake js string", - reason: 'should get the argument'); - }); - - testWidgets('evaluate Javascript with JavascriptMode disabled', - (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - // ignore: deprecated_member_use_from_same_package - () => controller.evaluateJavascript('fake js string'), - throwsA(anything), - ); - }); -*/ - testWidgets('runJavaScript', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - await controller.runJavascript('fake js string'); - expect(fakePlatformViewsController.lastCreatedView?.lastRunJavaScriptString, - 'fake js string'); - }); - - testWidgets('runJavaScript with JavascriptMode disabled', - (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - () => controller.runJavascript('fake js string'), - throwsA(anything), - ); +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const bool _skipDueToIssue86757 = true; + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; - testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { - late WebViewController controller; + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), ), ); - expect(await controller.runJavascriptReturningResult("fake js string"), - "fake js string", - reason: 'should get the argument'); - }); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); - testWidgets('runJavaScriptReturningResult with JavascriptMode disabled', - (WidgetTester tester) async { - late WebViewController controller; + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), ), ); - expect( - () => controller.runJavascriptReturningResult('fake js string'), - throwsA(anything), - ); - }); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); - testWidgets('Cookies can be cleared once', (WidgetTester tester) async { + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), ), ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); }); - testWidgets('Second cookie clear does not have cookies', - (WidgetTester tester) async { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), ), ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - final bool hasCookiesSecond = await cookieManager.clearCookies(); - expect(hasCookiesSecond, false); - }); - - testWidgets('Initial JavaScript channels', (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + await controller.runJavascript('Echo.postMessage("hello");'); + expect(messagesReceived, equals(['hello'])); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + + testWidgets('resize webview', (WidgetTester tester) async { + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } + }, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm'])); - }); - - test('Only valid JavaScript channel names are allowed', () { - final JavascriptMessageHandler noOp = (JavascriptMessage msg) {}; - JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); - JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); - JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); - - VoidCallback createChannel(String name) { - return () { - JavascriptChannel(name: name, onMessageReceived: noOp); - }; - } - - expect(createChannel('1Alarm'), throwsAssertionError); - expect(createChannel('foo.bar'), throwsAssertionError); - expect(createChannel(''), throwsAssertionError); + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); }); - testWidgets('Unique JavaScript channel names are required', - (WidgetTester tester) async { + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - expect(tester.takeException(), isNot(null)); - }); - - testWidgets('JavaScript channels update', (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - }, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), ), ); - + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), - }, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); }); - testWidgets('Remove all JavaScript channels and then add', - (WidgetTester tester) async { - // This covers a specific bug we had where after updating javascriptChannels to null, - // updating it again with a subset of the previously registered channels fails as the - // widget's cache of current channel wasn't properly updated when updating javascriptChannels to - // null. - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); + testWidgets('getTitle', (WidgetTester tester) async { + const String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); await tester.pumpWidget( - const WebView( - initialUrl: 'https://youtube.com', - ), - ); - - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts'])); - }); - - testWidgets('JavaScript channel messages', (WidgetTester tester) async { - final List ttsMessagesReceived = []; - final List alarmMessagesReceived = []; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', - onMessageReceived: (JavascriptMessage msg) { - ttsMessagesReceived.add(msg.message); - }), - JavascriptChannel( - name: 'Alarm', - onMessageReceived: (JavascriptMessage msg) { - alarmMessagesReceived.add(msg.message); - }), - }, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(ttsMessagesReceived, isEmpty); - expect(alarmMessagesReceived, isEmpty); - - platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); - platformWebView.fakeJavascriptPostMessage('Tts', 'World'); - - expect(ttsMessagesReceived, ['Hello', 'World']); - }); - - group('$PageStartedCallback', () { - testWidgets('onPageStarted is not null', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageStartedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - - testWidgets('onPageStarted is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - onPageStarted: null, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - // The platform side will always invoke a call for onPageStarted. This is - // to test that it does not crash on a null callback. - platformWebView.fakeOnPageStartedCallback(); - }); - - testWidgets('onPageStarted changed', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageStartedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - }); - - group('$PageFinishedCallback', () { - testWidgets('onPageFinished is not null', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageFinishedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - - testWidgets('onPageFinished is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - onPageFinished: null, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - // The platform side will always invoke a call for onPageFinished. This is - // to test that it does not crash on a null callback. - platformWebView.fakeOnPageFinishedCallback(); - }); - - testWidgets('onPageFinished changed', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageFinishedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - }); - - group('$PageLoadingCallback', () { - testWidgets('onLoadingProgress is not null', (WidgetTester tester) async { - int? loadingProgress; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onProgress: (int progress) { - loadingProgress = progress; - }, - )); - - final FakePlatformWebView? platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView?.fakeOnProgressCallback(50); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); - expect(loadingProgress, 50); - }); - - testWidgets('onLoadingProgress is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - onProgress: null, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - // This is to test that it does not crash on a null callback. - platformWebView.fakeOnProgressCallback(50); - }); - - testWidgets('onLoadingProgress changed', (WidgetTester tester) async { - int? loadingProgress; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onProgress: (int progress) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onProgress: (int progress) { - loadingProgress = progress; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnProgressCallback(50); - - expect(loadingProgress, 50); - }); - }); - - group('navigationDelegate', () { - testWidgets('hasNavigationDelegate', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.hasNavigationDelegate, false); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest r) => - NavigationDecision.navigate, - )); - - expect(platformWebView.hasNavigationDelegate, true); - }); - - testWidgets('Block navigation', (WidgetTester tester) async { - final List navigationRequests = []; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest request) { - navigationRequests.add(request); - // Only allow navigating to https://flutter.dev - return request.url == 'https://flutter.dev' - ? NavigationDecision.navigate - : NavigationDecision.prevent; - })); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.hasNavigationDelegate, true); - - platformWebView.fakeNavigate('https://www.google.com'); - // The navigation delegate only allows navigation to https://flutter.dev - // so we should still be in https://youtube.com. - expect(platformWebView.currentUrl, 'https://youtube.com'); - expect(navigationRequests.length, 1); - expect(navigationRequests[0].url, 'https://www.google.com'); - expect(navigationRequests[0].isForMainFrame, true); - - platformWebView.fakeNavigate('https://flutter.dev'); - await tester.pump(); - expect(platformWebView.currentUrl, 'https://flutter.dev'); - }); - }); - - group('debuggingEnabled', () { - testWidgets('enable debugging', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - debuggingEnabled: true, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.debuggingEnabled, true); - }); - - testWidgets('defaults to false', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.debuggingEnabled, false); - }); - - testWidgets('can be changed', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(WebView(key: key)); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - await tester.pumpWidget(WebView( - key: key, - debuggingEnabled: true, - )); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); - expect(platformWebView.debuggingEnabled, true); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); + + group('NavigationDelegate', () { + const String blankPage = ''; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); - await tester.pumpWidget(WebView( - key: key, - debuggingEnabled: false, - )); + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); - expect(platformWebView.debuggingEnabled, false); + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); }); - }); - - // Currently, webview for tizen cannot satisfy this test due to its implementation limitations. - /* - group('zoomEnabled', () { - testWidgets('Enable zoom', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - zoomEnabled: true, - )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); - expect(platformWebView.zoomEnabled, isTrue); - }); - - testWidgets('defaults to true', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); - expect(platformWebView.zoomEnabled, isTrue); + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } }); - testWidgets('can be changed', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(WebView(key: key)); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); - await tester.pumpWidget(WebView( - key: key, - zoomEnabled: true, - )); - - expect(platformWebView.zoomEnabled, isTrue); - - await tester.pumpWidget(WebView( - key: key, - zoomEnabled: false, - )); - - expect(platformWebView.zoomEnabled, isFalse); - }); - }); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); - group('Custom platform implementation', () { - setUpAll(() { - WebView.platform = MyWebViewPlatform(); - }); - tearDownAll(() { - WebView.platform = null; + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; }); - testWidgets('creation', (WidgetTester tester) async { + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); await tester.pumpWidget( - const WebView( - initialUrl: 'https://youtube.com', - gestureNavigationEnabled: true, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), ), ); - final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; - - expect( - platform.creationParams, - MatchesCreationParams(CreationParams( - initialUrl: 'https://youtube.com', - webSettings: WebSettings( - javascriptMode: JavascriptMode.disabled, - hasNavigationDelegate: false, - debuggingEnabled: false, - userAgent: WebSetting.of(null), - gestureNavigationEnabled: true, - zoomEnabled: true, - ), - ))); + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .runJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); }); - testWidgets('loadUrl', (WidgetTester tester) async { - late WebViewController controller; + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), ), ); - final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; - - final Map headers = { - 'header': 'value', - }; - - await controller.loadUrl('https://google.com', headers: headers); + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); - expect(platform.lastUrlLoaded, 'https://google.com'); - expect(platform.lastRequestHeaders, headers); + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); }); }); - */ - testWidgets('Set UserAgent', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.userAgent, isNull); - - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'UA', - )); - - expect(platformWebView.userAgent, 'UA'); - }); -} - -class FakePlatformWebView { - FakePlatformWebView(int? id, Map params) { - if (params.containsKey('initialUrl')) { - final String? initialUrl = params['initialUrl']; - if (initialUrl != null) { - history.add(initialUrl); - currentPosition++; - } - } - if (params.containsKey('javascriptChannelNames')) { - javascriptChannelNames = - List.from(params['javascriptChannelNames']); - } - javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; - hasNavigationDelegate = - params['settings']['hasNavigationDelegate'] ?? false; - debuggingEnabled = params['settings']['debuggingEnabled']; - userAgent = params['settings']['userAgent']; - zoomEnabled = params['settings']['zoomEnabled'] ?? true; - channel = MethodChannel( - 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); - channel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel channel; - - List history = []; - int currentPosition = -1; - int amountOfReloadsOnCurrentUrl = 0; - bool hasCache = true; - - String? get currentUrl => history.isEmpty ? null : history[currentPosition]; - JavascriptMode? javascriptMode; - List? javascriptChannelNames; - - bool? hasNavigationDelegate; - bool? debuggingEnabled; - String? userAgent; - bool? zoomEnabled; - - String? lastRunJavaScriptString; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'loadUrl': - final Map request = call.arguments; - _loadUrl(request['url']); - return Future.sync(() {}); - case 'updateSettings': - if (call.arguments['jsMode'] != null) { - javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; - } - if (call.arguments['hasNavigationDelegate'] != null) { - hasNavigationDelegate = call.arguments['hasNavigationDelegate']; - } - if (call.arguments['debuggingEnabled'] != null) { - debuggingEnabled = call.arguments['debuggingEnabled']; - } - userAgent = call.arguments['userAgent']; - if (call.arguments['zoomEnabled'] != null) { - zoomEnabled = call.arguments['zoomEnabled']; - } - break; - case 'canGoBack': - return Future.sync(() => currentPosition > 0); - case 'canGoForward': - return Future.sync(() => currentPosition < history.length - 1); - case 'goBack': - currentPosition = max(-1, currentPosition - 1); - return Future.sync(() {}); - case 'goForward': - currentPosition = min(history.length - 1, currentPosition + 1); - return Future.sync(() {}); - case 'reload': - amountOfReloadsOnCurrentUrl++; - return Future.sync(() {}); - case 'currentUrl': - return Future.value(currentUrl); - case 'runJavascriptReturningResult': - case 'evaluateJavascript': - lastRunJavaScriptString = call.arguments; - return Future.value(call.arguments); - case 'runJavascript': - lastRunJavaScriptString = call.arguments; - return Future.sync(() {}); - case 'addJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames!.addAll(channelNames); - break; - case 'removeJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames! - .removeWhere((String channel) => channelNames.contains(channel)); - break; - case 'clearCache': - hasCache = false; - return Future.sync(() {}); - } - return Future.sync(() {}); - } - - void fakeJavascriptPostMessage(String jsChannel, String message) { - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'channel': jsChannel, - 'message': message - }; - final ByteData data = codec - .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - // Fakes a main frame navigation that was initiated by the webview, e.g when - // the user clicks a link in the currently loaded page. - void fakeNavigate(String url) { - if (!hasNavigationDelegate!) { - print('no navigation delegate'); - _loadUrl(url); - return; - } - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'url': url, - 'isForMainFrame': true - }; - final ByteData data = - codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) { - final bool allow = codec.decodeEnvelope(data!); - if (allow) { - _loadUrl(url); - } - }); - } - - void fakeOnPageStartedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageStarted', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnPageFinishedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageFinished', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnProgressCallback(int progress) { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onProgress', - {'progress': progress}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - void _loadUrl(String? url) { - history = history.sublist(0, currentPosition + 1); - history.add(url); - currentPosition++; - amountOfReloadsOnCurrentUrl = 0; - } -} -class _FakePlatformViewsController { - FakePlatformWebView? lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = call.arguments; - final Map params = _decodeParams(args['params'])!; - lastCreatedView = FakePlatformWebView( - args['id'], - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } - - void reset() { - lastCreatedView = null; - } -} - -Map? _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757 + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.runJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion(primaryUrl)); + }, + skip: _skipDueToIssue86757, ); - return const StandardMessageCodec().decodeMessage(messageBytes); } -class _FakeCookieManager { - _FakeCookieManager() { - final MethodChannel channel = const MethodChannel( - 'plugins.flutter.io/cookie_manager', - StandardMethodCodec(), - ); - channel.setMockMethodCallHandler(onMethodCall); - } - - bool hasCookies = true; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'clearCookies': - bool hadCookies = false; - if (hasCookies) { - hadCookies = true; - hasCookies = false; - } - return Future.sync(() { - return hadCookies; - }); - } - return Future.sync(() => true); - } - - void reset() { - hasCookies = true; - } +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); } -class MyWebViewPlatform implements WebViewPlatform { - MyWebViewPlatformController? lastPlatformBuilt; - - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - required JavascriptChannelRegistry javascriptChannelRegistry, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(onWebViewPlatformCreated != null); - lastPlatformBuilt = MyWebViewPlatformController( - creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); - onWebViewPlatformCreated!(lastPlatformBuilt); - return Container(); - } - - @override - Future clearCookies() { - return Future.sync(() => true); - } -} - -class MyWebViewPlatformController extends WebViewPlatformController { - MyWebViewPlatformController(this.creationParams, this.gestureRecognizers, - WebViewPlatformCallbacksHandler platformHandler) - : super(platformHandler); - - CreationParams? creationParams; - Set>? gestureRecognizers; - - String? lastUrlLoaded; - Map? lastRequestHeaders; - - @override - Future loadUrl(String url, Map? headers) async { - equals(1, 1); - lastUrlLoaded = url; - lastRequestHeaders = headers; - } +Future _runJavascriptReturningResult( + WebViewController controller, String js) async { + return await controller.runJavascriptReturningResult(js); } -class MatchesWebSettings extends Matcher { - MatchesWebSettings(this._webSettings); +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); - final WebSettings? _webSettings; + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; @override - Description describe(Description description) => - description.add('$_webSettings'); - - @override - bool matches( - covariant WebSettings webSettings, Map matchState) { - return _webSettings!.javascriptMode == webSettings.javascriptMode && - _webSettings!.hasNavigationDelegate == - webSettings.hasNavigationDelegate && - _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && - _webSettings!.gestureNavigationEnabled == - webSettings.gestureNavigationEnabled && - _webSettings!.userAgent == webSettings.userAgent && - _webSettings!.zoomEnabled == webSettings.zoomEnabled; - } + State createState() => ResizableWebViewState(); } -class MatchesCreationParams extends Matcher { - MatchesCreationParams(this._creationParams); - - final CreationParams _creationParams; +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; @override - Description describe(Description description) => - description.add('$_creationParams'); - - @override - bool matches(covariant CreationParams creationParams, - Map matchState) { - return _creationParams.initialUrl == creationParams.initialUrl && - MatchesWebSettings(_creationParams.webSettings) - .matches(creationParams.webSettings!, matchState) && - orderedEquals(_creationParams.javascriptChannelNames) - .matches(creationParams.javascriptChannelNames, matchState); + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); } } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index fc9d1ee0b..2527a85b7 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -12,7 +12,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_tizen/webview_flutter_tizen.dart'; void main() => runApp(const MaterialApp(home: WebViewExample())); @@ -41,8 +40,8 @@ const String kLocalExamplePage = '''

Local demo page

- This is an example page used to demonstrate how to load a local file or HTML - string using the Flutter + This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter webview plugin.

@@ -72,12 +71,12 @@ const String kTransparentBackgroundPage = ''' '''; class WebViewExample extends StatefulWidget { - const WebViewExample({this.cookieManager}); + const WebViewExample({Key? key, this.cookieManager}) : super(key: key); final CookieManager? cookieManager; @override - _WebViewExampleState createState() => _WebViewExampleState(); + State createState() => _WebViewExampleState(); } class _WebViewExampleState extends State { @@ -153,7 +152,7 @@ class _WebViewExampleState extends State { onPressed: () async { String? url; if (controller.hasData) { - url = (await controller.data!.currentUrl())!; + url = await controller.data!.currentUrl(); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -188,11 +187,12 @@ enum MenuOptions { } class SampleMenu extends StatelessWidget { - SampleMenu(this.controller, CookieManager? cookieManager) - : cookieManager = CookieManagerTizen(); + SampleMenu(this.controller, CookieManager? cookieManager, {Key? key}) + : cookieManager = cookieManager ?? CookieManager(), + super(key: key); final Future controller; - late final CookieManagerTizen cookieManager; + late final CookieManager cookieManager; @override Widget build(BuildContext context) { @@ -248,8 +248,8 @@ class SampleMenu extends StatelessWidget { itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: MenuOptions.showUserAgent, - child: const Text('Show user agent'), enabled: controller.hasData, + child: const Text('Show user agent'), ), const PopupMenuItem( value: MenuOptions.listCookies, @@ -342,6 +342,7 @@ class SampleMenu extends StatelessWidget { Future _onListCache( WebViewController controller, BuildContext context) async { await controller.runJavascript('caches.keys()' + // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } @@ -441,8 +442,9 @@ class SampleMenu extends StatelessWidget { } class NavigationControls extends StatelessWidget { - const NavigationControls(this._webViewControllerFuture) - : assert(_webViewControllerFuture != null); + const NavigationControls(this._webViewControllerFuture, {Key? key}) + : assert(_webViewControllerFuture != null), + super(key: key); final Future _webViewControllerFuture; diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml index 76d926a83..acc1227ce 100644 --- a/packages/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/example/pubspec.yaml @@ -1,16 +1,17 @@ name: webview_flutter_tizen_example description: Demonstrates how to use the webview_flutter_tizen plugin. +publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" dependencies: flutter: sdk: flutter path_provider: ^2.0.7 path_provider_tizen: ^2.0.2 - webview_flutter: ^3.0.2 + webview_flutter: ^3.0.4 webview_flutter_tizen: path: ../ diff --git a/packages/webview_flutter/example/test_driver/integration_test.dart b/packages/webview_flutter/example/test_driver/integration_test.dart index e17afb0de..4f10f2a52 100644 --- a/packages/webview_flutter/example/test_driver/integration_test.dart +++ b/packages/webview_flutter/example/test_driver/integration_test.dart @@ -1,4 +1,6 @@ -// @dart = 2.9 +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'package:integration_test/integration_test_driver.dart'; diff --git a/packages/webview_flutter/lib/src/platform_view.dart b/packages/webview_flutter/lib/src/platform_view.dart deleted file mode 100644 index e5474e966..000000000 --- a/packages/webview_flutter/lib/src/platform_view.dart +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// github.com:flutter/flutter.git@02c026b03cd31dd3f867e5faeb7e104cce174c5f -// packages/flutter/lib/src/rendering/platform_view.dart -// Imported from above file, the content has not been modified. - -part of '../webview_flutter_tizen.dart'; - -enum _PlatformViewState { - uninitialized, - resizing, - ready, -} - -bool _factoryTypesSetEquals(Set>? a, Set>? b) { - if (a == b) { - return true; - } - if (a == null || b == null) { - return false; - } - return setEquals(_factoriesTypeSet(a), _factoriesTypeSet(b)); -} - -Set _factoriesTypeSet(Set> factories) { - return factories.map((Factory factory) => factory.type).toSet(); -} - -typedef _HandlePointerEvent = Future Function(PointerEvent event); - -// This recognizer constructs gesture recognizers from a set of gesture recognizer factories -// it was give, adds all of them to a gesture arena team with the _PlatformViewGestureRecognizer -// as the team captain. -// As long as the gesture arena is unresolved, the recognizer caches all pointer events. -// When the team wins, the recognizer sends all the cached pointer events to `_handlePointerEvent`, and -// sets itself to a "forwarding mode" where it will forward any new pointer event to `_handlePointerEvent`. -class _PlatformViewGestureRecognizer extends OneSequenceGestureRecognizer { - _PlatformViewGestureRecognizer( - _HandlePointerEvent handlePointerEvent, - this.gestureRecognizerFactories, { - Set? supportedDevices, - }) : super(supportedDevices: supportedDevices) { - team = GestureArenaTeam()..captain = this; - _gestureRecognizers = gestureRecognizerFactories.map( - (Factory recognizerFactory) { - final OneSequenceGestureRecognizer gestureRecognizer = - recognizerFactory.constructor(); - gestureRecognizer.team = team; - // The below gesture recognizers requires at least one non-empty callback to - // compete in the gesture arena. - // https://github.com/flutter/flutter/issues/35394#issuecomment-562285087 - if (gestureRecognizer is LongPressGestureRecognizer) { - gestureRecognizer.onLongPress ??= () {}; - } else if (gestureRecognizer is DragGestureRecognizer) { - gestureRecognizer.onDown ??= (_) {}; - } else if (gestureRecognizer is TapGestureRecognizer) { - gestureRecognizer.onTapDown ??= (_) {}; - } - return gestureRecognizer; - }, - ).toSet(); - _handlePointerEvent = handlePointerEvent; - } - - late _HandlePointerEvent _handlePointerEvent; - - // Maps a pointer to a list of its cached pointer events. - // Before the arena for a pointer is resolved all events are cached here, if we win the arena - // the cached events are dispatched to `_handlePointerEvent`, if we lose the arena we clear the cache for - // the pointer. - final Map> cachedEvents = >{}; - - // Pointer for which we have already won the arena, events for pointers in this set are - // immediately dispatched to `_handlePointerEvent`. - final Set forwardedPointers = {}; - - // We use OneSequenceGestureRecognizers as they support gesture arena teams. - // TODO(amirh): get a list of GestureRecognizers here. - // https://github.com/flutter/flutter/issues/20953 - final Set> gestureRecognizerFactories; - late Set _gestureRecognizers; - - @override - void addAllowedPointer(PointerDownEvent event) { - startTrackingPointer(event.pointer, event.transform); - for (final OneSequenceGestureRecognizer recognizer in _gestureRecognizers) { - recognizer.addPointer(event); - } - } - - @override - String get debugDescription => 'Platform view'; - - @override - void didStopTrackingLastPointer(int pointer) {} - - @override - void handleEvent(PointerEvent event) { - if (!forwardedPointers.contains(event.pointer)) { - _cacheEvent(event); - } else { - _handlePointerEvent(event); - } - stopTrackingIfPointerNoLongerDown(event); - } - - @override - void acceptGesture(int pointer) { - _flushPointerCache(pointer); - forwardedPointers.add(pointer); - } - - @override - void rejectGesture(int pointer) { - stopTrackingPointer(pointer); - cachedEvents.remove(pointer); - } - - void _cacheEvent(PointerEvent event) { - if (!cachedEvents.containsKey(event.pointer)) { - cachedEvents[event.pointer] = []; - } - cachedEvents[event.pointer]!.add(event); - } - - void _flushPointerCache(int pointer) { - cachedEvents.remove(pointer)?.forEach(_handlePointerEvent); - } - - @override - void stopTrackingPointer(int pointer) { - super.stopTrackingPointer(pointer); - forwardedPointers.remove(pointer); - } - - void reset() { - forwardedPointers.forEach(super.stopTrackingPointer); - forwardedPointers.clear(); - cachedEvents.keys.forEach(super.stopTrackingPointer); - cachedEvents.clear(); - resolve(GestureDisposition.rejected); - } -} - -/// The Mixin handling the pointer events and gestures of a platform view render box. -mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { - /// How to behave during hit testing. - // Changing _hitTestBehavior might affect which objects are considered hovered over. - set hitTestBehavior(PlatformViewHitTestBehavior value) { - if (value != _hitTestBehavior) { - _hitTestBehavior = value; - if (owner != null) markNeedsPaint(); - } - } - - PlatformViewHitTestBehavior? _hitTestBehavior; - - _HandlePointerEvent? _handlePointerEvent; - - /// Any active gesture arena the `PlatformView` participates in is rejected when the - /// set of gesture recognizers is changed. - void _updateGestureRecognizersWithCallBack( - Set> gestureRecognizers, - _HandlePointerEvent handlePointerEvent) { - assert(gestureRecognizers != null); - assert( - _factoriesTypeSet(gestureRecognizers).length == gestureRecognizers.length, - 'There were multiple gesture recognizer factories for the same type, there must only be a single ' - 'gesture recognizer factory for each gesture recognizer type.', - ); - if (_factoryTypesSetEquals( - gestureRecognizers, _gestureRecognizer?.gestureRecognizerFactories)) { - return; - } - _gestureRecognizer?.dispose(); - _gestureRecognizer = - _PlatformViewGestureRecognizer(handlePointerEvent, gestureRecognizers); - _handlePointerEvent = handlePointerEvent; - } - - _PlatformViewGestureRecognizer? _gestureRecognizer; - - @override - bool hitTest(BoxHitTestResult result, {required Offset position}) { - if (_hitTestBehavior == PlatformViewHitTestBehavior.transparent || - !size.contains(position)) { - return false; - } - result.add(BoxHitTestEntry(this, position)); - return _hitTestBehavior == PlatformViewHitTestBehavior.opaque; - } - - @override - bool hitTestSelf(Offset position) => - _hitTestBehavior != PlatformViewHitTestBehavior.transparent; - - @override - PointerEnterEventListener? get onEnter => null; - - @override - PointerExitEventListener? get onExit => null; - - @override - MouseCursor get cursor => MouseCursor.uncontrolled; - - @override - bool get validForMouseTracker => true; - - @override - void handleEvent(PointerEvent event, HitTestEntry entry) { - if (event is PointerDownEvent) { - _gestureRecognizer!.addPointer(event); - } - if (event is PointerHoverEvent) { - _handlePointerEvent?.call(event); - } - } - - @override - void detach() { - _gestureRecognizer!.reset(); - super.detach(); - } -} diff --git a/packages/webview_flutter/lib/src/platform_view_tizen.dart b/packages/webview_flutter/lib/src/platform_view_tizen.dart index 6ce37260a..61b6d2059 100644 --- a/packages/webview_flutter/lib/src/platform_view_tizen.dart +++ b/packages/webview_flutter/lib/src/platform_view_tizen.dart @@ -3,7 +3,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// github.com:flutter/flutter.git@02c026b03cd31dd3f867e5faeb7e104cce174c5f +// github.com:flutter/flutter.git@57a688c1f04d56eaa40beeb9f44e549eaf0ce54d // packages/flutter/lib/src/rendering/platform_view.dart // packages/flutter/lib/src/widgets/platform_view.dart // packages/flutter/lib/src/services/platform_views.dart @@ -14,9 +14,15 @@ part of '../webview_flutter_tizen.dart'; +enum _PlatformViewState { + uninitialized, + resizing, + ready, +} + class TizenView extends StatefulWidget { const TizenView({ - Key? key, + super.key, required this.viewType, this.onPlatformViewCreated, this.hitTestBehavior = PlatformViewHitTestBehavior.opaque, @@ -27,8 +33,7 @@ class TizenView extends StatefulWidget { this.clipBehavior = Clip.hardEdge, }) : assert(viewType != null), assert(hitTestBehavior != null), - assert(creationParams == null || creationParamsCodec != null), - super(key: key); + assert(creationParams == null || creationParamsCodec != null); final String viewType; final PlatformViewCreatedCallback? onPlatformViewCreated; @@ -144,7 +149,6 @@ class _TizenWebViewState extends State { if (!_controller.isCreated) { return; } - if (!isFocused) { _controller.clearFocus().catchError((dynamic e) { if (e is MissingPluginException) { @@ -153,12 +157,10 @@ class _TizenWebViewState extends State { }); return; } - SystemChannels.textInput - .invokeMethod( + SystemChannels.textInput.invokeMethod( 'TextInput.setPlatformViewClient', - _id, - ) - .catchError((dynamic e) { + {'platformViewId': _id}, + ).catchError((dynamic e) { if (e is MissingPluginException) { return; } @@ -217,36 +219,66 @@ class TizenViewController extends PlatformViewController { int? get textureId => _textureId; - late Size _size; + /// The current offset of the platform view. + Offset _off = Offset.zero; - Future setSize(Size size) async { + Future setSize(Size size) async { assert(_state != _TizenViewState.disposed, - 'trying to size a disposed Tizen View. View id: $viewId'); - + 'Tizen view is disposed. View id: $viewId'); + assert(_state != _TizenViewState.waitingForSize, + 'Tizen view must have an initial size. View id: $viewId'); assert(size != null); assert(!size.isEmpty); - if (_state == _TizenViewState.waitingForSize) { - _size = size; - return create(); + final Map? meta = + await SystemChannels.platform_views.invokeMapMethod( + 'resize', + { + 'id': viewId, + 'width': size.width, + 'height': size.height, + }, + ); + assert(meta != null); + assert(meta!.containsKey('width')); + assert(meta!.containsKey('height')); + return Size(meta!['width']! as double, meta['height']! as double); + } + + Future setOffset(Offset off) async { + if (off == _off) { + return; } - await SystemChannels.platform_views - .invokeMethod('resize', { - 'id': viewId, - 'width': size.width, - 'height': size.height, - }); + + if (_state != _TizenViewState.created) { + return; + } + + _off = off; + + await SystemChannels.platform_views.invokeMethod( + 'offset', + { + 'id': viewId, + 'top': off.dy, + 'left': off.dx, + }, + ); } - Future _sendCreateMessage() async { - assert(!_size.isEmpty, + Future _sendCreateMessage({Size? size}) async { + if (size == null) { + return; + } + + assert(!size.isEmpty, 'trying to create $TizenViewController without setting a valid size.'); final Map args = { 'id': viewId, 'viewType': _viewType, - 'width': _size.width, - 'height': _size.height, + 'width': size.width, + 'height': size.height, 'direction': _layoutDirection == TextDirection.ltr ? 0 : 1, }; if (_creationParams != null) { @@ -270,10 +302,10 @@ class TizenViewController extends PlatformViewController { }); } - Future create() async { + Future create({Size? size}) async { assert(_state != _TizenViewState.disposed, 'trying to create a disposed Tizen view'); - await _sendCreateMessage(); + await _sendCreateMessage(size: size); _state = _TizenViewState.created; for (final PlatformViewCreatedCallback callback @@ -282,14 +314,6 @@ class TizenViewController extends PlatformViewController { } } - @Deprecated('Call `controller.viewId` instead. ' - 'This feature was deprecated after v1.20.0-2.0.pre.') - int get id => viewId; - - set pointTransformer(PointTransformer transformer) { - assert(transformer != null); - } - bool get isCreated => _state == _TizenViewState.created; void addOnPlatformViewCreatedListener(PlatformViewCreatedCallback listener) { @@ -435,7 +459,7 @@ class PlatformViewsServiceTizen { /// See also: /// /// * [PlatformViewsService] which is a service for controlling platform views. -class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { +class RenderTizenView extends PlatformViewRenderBox { /// Creates a render object for an Tizen view. RenderTizenView({ required TizenViewController viewController, @@ -445,9 +469,13 @@ class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { }) : assert(viewController != null), assert(hitTestBehavior != null), assert(gestureRecognizers != null), + assert(clipBehavior != null), _viewController = viewController, - _clipBehavior = clipBehavior { - _viewController.pointTransformer = (Offset offset) => globalToLocal(offset); + _clipBehavior = clipBehavior, + super( + controller: viewController, + hitTestBehavior: hitTestBehavior, + gestureRecognizers: gestureRecognizers) { updateGestureRecognizers(gestureRecognizers); _viewController.addOnPlatformViewCreatedListener(_onPlatformViewCreated); this.hitTestBehavior = hitTestBehavior; @@ -455,13 +483,18 @@ class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { _PlatformViewState _state = _PlatformViewState.uninitialized; - TizenViewController get viewcontroller => _viewController; + Size? _currentTextureSize; + + @override + TizenViewController get controller => _viewController; + TizenViewController _viewController; /// Sets a new Tizen view controller. /// /// `viewController` must not be null. - set viewController(TizenViewController viewController) { + @override + set controller(TizenViewController viewController) { assert(_viewController != null); assert(viewController != null); if (_viewController == viewController) { @@ -494,26 +527,6 @@ class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { markNeedsSemanticsUpdate(); } - /// {@template flutter.rendering.RenderTizenView.updateGestureRecognizers} - /// Updates which gestures should be forwarded to the platform view. - /// - /// Gesture recognizers created by factories in this set participate in the gesture arena for each - /// pointer that was put down on the render box. If any of the recognizers on this list wins the - /// gesture arena, the entire pointer event sequence starting from the pointer down event - /// will be dispatched to the Tizen. - /// - /// The `gestureRecognizers` property must not contain more than one factory with the same [Factory.type]. - /// - /// Setting a new set of gesture recognizer factories with the same [Factory.type]s as the current - /// set has no effect, because the factories' constructors would have already been called with the previous set. - /// {@endtemplate} - /// - void updateGestureRecognizers( - Set> gestureRecognizers) { - _updateGestureRecognizersWithCallBack( - gestureRecognizers, _viewController.dispatchPointerEvent); - } - @override bool get sizedByParent => true; @@ -523,26 +536,34 @@ class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { @override bool get isRepaintBoundary => true; + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.biggest; + } + @override void performResize() { - size = constraints.biggest; + super.performResize(); _sizePlatformView(); } - late Size _currentTizenViewSize; - Future _sizePlatformView() async { if (_state == _PlatformViewState.resizing || size.isEmpty) { return; } + _state = _PlatformViewState.resizing; markNeedsPaint(); Size targetSize; do { targetSize = size; - await _viewController.setSize(targetSize); - _currentTizenViewSize = targetSize; + if (_viewController.isCreated) { + _currentTextureSize = await _viewController.setSize(targetSize); + } else { + await _viewController.create(size: targetSize); + _currentTextureSize = targetSize; + } } while (size != targetSize); _state = _PlatformViewState.ready; @@ -551,28 +572,44 @@ class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { @override void paint(PaintingContext context, Offset offset) { - if (_viewController.textureId == null) { + if (_viewController.textureId == null || _currentTextureSize == null) return; - } - if ((size.width < _currentTizenViewSize.width || - size.height < _currentTizenViewSize.height) && - clipBehavior != Clip.none) { - _clipRectLayer = context.pushClipRect( - true, offset, offset & size, _paintTexture, - clipBehavior: clipBehavior, oldLayer: _clipRectLayer); + + final bool isTextureLargerThanWidget = + _currentTextureSize!.width > size.width || + _currentTextureSize!.height > size.height; + if (isTextureLargerThanWidget && clipBehavior != Clip.none) { + _clipRectLayer.layer = context.pushClipRect( + true, + offset, + offset & size, + _paintTexture, + clipBehavior: clipBehavior, + oldLayer: _clipRectLayer.layer, + ); return; } - _clipRectLayer = null; + _clipRectLayer.layer = null; _paintTexture(context, offset); } - ClipRectLayer? _clipRectLayer; + final LayerHandle _clipRectLayer = + LayerHandle(); + + @override + void dispose() { + _clipRectLayer.layer = null; + super.dispose(); + } void _paintTexture(PaintingContext context, Offset offset) { + if (_currentTextureSize == null) { + return; + } + context.addLayer(TextureLayer( - rect: offset & _currentTizenViewSize, + rect: offset & _currentTextureSize!, textureId: _viewController.textureId!, - freeze: _state == _PlatformViewState.resizing, )); } @@ -590,15 +627,13 @@ class RenderTizenView extends RenderBox with _PlatformViewGestureMixin { class _TizenPlatformTextureView extends LeafRenderObjectWidget { const _TizenPlatformTextureView({ - Key? key, required this.controller, required this.hitTestBehavior, required this.gestureRecognizers, this.clipBehavior = Clip.hardEdge, }) : assert(controller != null), assert(hitTestBehavior != null), - assert(gestureRecognizers != null), - super(key: key); + assert(gestureRecognizers != null); final TizenViewController controller; final PlatformViewHitTestBehavior hitTestBehavior; @@ -615,7 +650,7 @@ class _TizenPlatformTextureView extends LeafRenderObjectWidget { @override void updateRenderObject(BuildContext context, RenderTizenView renderObject) { - renderObject.viewController = controller; + renderObject.controller = controller; renderObject.hitTestBehavior = hitTestBehavior; renderObject.updateGestureRecognizers(gestureRecognizers); renderObject.clipBehavior = clipBehavior; diff --git a/packages/webview_flutter/lib/webview_flutter_tizen.dart b/packages/webview_flutter/lib/webview_flutter_tizen.dart index 470288bd1..eea380669 100644 --- a/packages/webview_flutter/lib/webview_flutter_tizen.dart +++ b/packages/webview_flutter/lib/webview_flutter_tizen.dart @@ -8,14 +8,13 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter/rendering.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -part 'src/platform_view.dart'; part 'src/platform_view_tizen.dart'; /// Builds an Tizen webview. @@ -26,6 +25,7 @@ class TizenWebView implements WebViewPlatform { /// Sets a tizen [WebViewPlatform]. static void register() { WebView.platform = TizenWebView(); + WebViewCookieManagerPlatform.instance = WebViewTizenCookieManager(); } @override @@ -96,31 +96,3 @@ class WebViewTizenCookieManager extends WebViewCookieManagerPlatform { return true; } } - -/// Manages cookies pertaining to all [WebView]s. -class CookieManagerTizen { - /// Creates a [CookieManager] -- returns the instance if it's already been called. - factory CookieManagerTizen() { - return _instance ??= CookieManagerTizen._(); - } - - CookieManagerTizen._() { - if (WebViewCookieManagerPlatform.instance == null) { - WebViewCookieManagerPlatform.instance = WebViewTizenCookieManager(); - } - } - - static CookieManagerTizen? _instance; - - /// Clears all cookies for all [WebView] instances. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => - WebViewCookieManagerPlatform.instance!.clearCookies(); - - /// Sets a cookie for all [WebView] instances. - /// - /// This is a no op on iOS versions below 11. - Future setCookie(WebViewCookie cookie) => - WebViewCookieManagerPlatform.instance!.setCookie(cookie); -} diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 0b87fa765..8c8d57f80 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -2,27 +2,26 @@ name: webview_flutter_tizen description: Tizen implementation of the webview plugin homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/webview_flutter -version: 0.4.4 +version: 0.5.0 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=2.8.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: WebviewFlutterTizenPlugin + fileName: webview_flutter_tizen_plugin.h + dartPluginClass: TizenWebView dependencies: flutter: sdk: flutter + webview_flutter: ^3.0.4 webview_flutter_platform_interface: ^1.8.0 - webview_flutter: ^3.0.2 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 - -flutter: - plugin: - platforms: - tizen: - pluginClass: WebviewFlutterTizenPlugin - fileName: webview_flutter_tizen_plugin.h - dartPluginClass: TizenWebView diff --git a/packages/webview_flutter/tizen/lib/aarch64/liblightweight-web-engine.flutter.so b/packages/webview_flutter/tizen/lib/aarch64/liblightweight-web-engine.flutter.so index a79820c54..ad2e8bd2e 100755 Binary files a/packages/webview_flutter/tizen/lib/aarch64/liblightweight-web-engine.flutter.so and b/packages/webview_flutter/tizen/lib/aarch64/liblightweight-web-engine.flutter.so differ diff --git a/packages/webview_flutter/tizen/lib/armel/liblightweight-web-engine.flutter.so b/packages/webview_flutter/tizen/lib/armel/liblightweight-web-engine.flutter.so index 08f9bc7ac..e7fa2cbe7 100755 Binary files a/packages/webview_flutter/tizen/lib/armel/liblightweight-web-engine.flutter.so and b/packages/webview_flutter/tizen/lib/armel/liblightweight-web-engine.flutter.so differ diff --git a/packages/webview_flutter/tizen/lib/i586/liblightweight-web-engine.flutter.so b/packages/webview_flutter/tizen/lib/i586/liblightweight-web-engine.flutter.so index 6ff012d28..b484227f3 100755 Binary files a/packages/webview_flutter/tizen/lib/i586/liblightweight-web-engine.flutter.so and b/packages/webview_flutter/tizen/lib/i586/liblightweight-web-engine.flutter.so differ