webview_flutter_test.dart 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211
  1. // Copyright 2018 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:math';
  5. import 'dart:typed_data';
  6. import 'package:flutter/services.dart';
  7. import 'package:flutter/src/foundation/basic_types.dart';
  8. import 'package:flutter/src/gestures/recognizer.dart';
  9. import 'package:flutter/widgets.dart';
  10. import 'package:flutter_test/flutter_test.dart';
  11. import 'package:webview_flutter/platform_interface.dart';
  12. import 'package:webview_flutter/webview_flutter.dart';
  13. typedef void VoidCallback();
  14. void main() {
  15. TestWidgetsFlutterBinding.ensureInitialized();
  16. final _FakePlatformViewsController fakePlatformViewsController =
  17. _FakePlatformViewsController();
  18. final _FakeCookieManager _fakeCookieManager = _FakeCookieManager();
  19. setUpAll(() {
  20. SystemChannels.platform_views.setMockMethodCallHandler(
  21. fakePlatformViewsController.fakePlatformViewsMethodHandler);
  22. SystemChannels.platform
  23. .setMockMethodCallHandler(_fakeCookieManager.onMethodCall);
  24. });
  25. setUp(() {
  26. fakePlatformViewsController.reset();
  27. _fakeCookieManager.reset();
  28. });
  29. testWidgets('Create WebView', (WidgetTester tester) async {
  30. await tester.pumpWidget(const WebView());
  31. });
  32. testWidgets('Initial url', (WidgetTester tester) async {
  33. WebViewController controller;
  34. await tester.pumpWidget(
  35. WebView(
  36. initialUrl: 'https://youtube.com',
  37. onWebViewCreated: (WebViewController webViewController) {
  38. controller = webViewController;
  39. },
  40. ),
  41. );
  42. expect(await controller.currentUrl(), 'https://youtube.com');
  43. });
  44. testWidgets('Javascript mode', (WidgetTester tester) async {
  45. await tester.pumpWidget(const WebView(
  46. initialUrl: 'https://youtube.com',
  47. javascriptMode: JavascriptMode.unrestricted,
  48. ));
  49. final FakePlatformWebView platformWebView =
  50. fakePlatformViewsController.lastCreatedView;
  51. expect(platformWebView.javascriptMode, JavascriptMode.unrestricted);
  52. await tester.pumpWidget(const WebView(
  53. initialUrl: 'https://youtube.com',
  54. javascriptMode: JavascriptMode.disabled,
  55. ));
  56. expect(platformWebView.javascriptMode, JavascriptMode.disabled);
  57. });
  58. testWidgets('Load url', (WidgetTester tester) async {
  59. WebViewController controller;
  60. await tester.pumpWidget(
  61. WebView(
  62. onWebViewCreated: (WebViewController webViewController) {
  63. controller = webViewController;
  64. },
  65. ),
  66. );
  67. expect(controller, isNotNull);
  68. await controller.loadUrl('https://flutter.io');
  69. expect(await controller.currentUrl(), 'https://flutter.io');
  70. });
  71. testWidgets('Invalid urls', (WidgetTester tester) async {
  72. WebViewController controller;
  73. await tester.pumpWidget(
  74. WebView(
  75. onWebViewCreated: (WebViewController webViewController) {
  76. controller = webViewController;
  77. },
  78. ),
  79. );
  80. expect(controller, isNotNull);
  81. expect(() => controller.loadUrl(null), throwsA(anything));
  82. expect(await controller.currentUrl(), isNull);
  83. expect(() => controller.loadUrl(''), throwsA(anything));
  84. expect(await controller.currentUrl(), isNull);
  85. // Missing schema.
  86. expect(() => controller.loadUrl('flutter.io'), throwsA(anything));
  87. expect(await controller.currentUrl(), isNull);
  88. });
  89. testWidgets('Headers in loadUrl', (WidgetTester tester) async {
  90. WebViewController controller;
  91. await tester.pumpWidget(
  92. WebView(
  93. onWebViewCreated: (WebViewController webViewController) {
  94. controller = webViewController;
  95. },
  96. ),
  97. );
  98. expect(controller, isNotNull);
  99. final Map<String, String> headers = <String, String>{
  100. 'CACHE-CONTROL': 'ABC'
  101. };
  102. await controller.loadUrl('https://flutter.io', headers: headers);
  103. expect(await controller.currentUrl(), equals('https://flutter.io'));
  104. });
  105. testWidgets("Can't go back before loading a page",
  106. (WidgetTester tester) async {
  107. WebViewController controller;
  108. await tester.pumpWidget(
  109. WebView(
  110. onWebViewCreated: (WebViewController webViewController) {
  111. controller = webViewController;
  112. },
  113. ),
  114. );
  115. expect(controller, isNotNull);
  116. final bool canGoBackNoPageLoaded = await controller.canGoBack();
  117. expect(canGoBackNoPageLoaded, false);
  118. });
  119. testWidgets("Clear Cache", (WidgetTester tester) async {
  120. WebViewController controller;
  121. await tester.pumpWidget(
  122. WebView(
  123. onWebViewCreated: (WebViewController webViewController) {
  124. controller = webViewController;
  125. },
  126. ),
  127. );
  128. expect(controller, isNotNull);
  129. expect(fakePlatformViewsController.lastCreatedView.hasCache, true);
  130. await controller.clearCache();
  131. expect(fakePlatformViewsController.lastCreatedView.hasCache, false);
  132. });
  133. testWidgets("Can't go back with no history", (WidgetTester tester) async {
  134. WebViewController controller;
  135. await tester.pumpWidget(
  136. WebView(
  137. initialUrl: 'https://flutter.io',
  138. onWebViewCreated: (WebViewController webViewController) {
  139. controller = webViewController;
  140. },
  141. ),
  142. );
  143. expect(controller, isNotNull);
  144. final bool canGoBackFirstPageLoaded = await controller.canGoBack();
  145. expect(canGoBackFirstPageLoaded, false);
  146. });
  147. testWidgets('Can go back', (WidgetTester tester) async {
  148. WebViewController controller;
  149. await tester.pumpWidget(
  150. WebView(
  151. initialUrl: 'https://flutter.io',
  152. onWebViewCreated: (WebViewController webViewController) {
  153. controller = webViewController;
  154. },
  155. ),
  156. );
  157. expect(controller, isNotNull);
  158. await controller.loadUrl('https://www.google.com');
  159. final bool canGoBackSecondPageLoaded = await controller.canGoBack();
  160. expect(canGoBackSecondPageLoaded, true);
  161. });
  162. testWidgets("Can't go forward before loading a page",
  163. (WidgetTester tester) async {
  164. WebViewController controller;
  165. await tester.pumpWidget(
  166. WebView(
  167. onWebViewCreated: (WebViewController webViewController) {
  168. controller = webViewController;
  169. },
  170. ),
  171. );
  172. expect(controller, isNotNull);
  173. final bool canGoForwardNoPageLoaded = await controller.canGoForward();
  174. expect(canGoForwardNoPageLoaded, false);
  175. });
  176. testWidgets("Can't go forward with no history", (WidgetTester tester) async {
  177. WebViewController controller;
  178. await tester.pumpWidget(
  179. WebView(
  180. initialUrl: 'https://flutter.io',
  181. onWebViewCreated: (WebViewController webViewController) {
  182. controller = webViewController;
  183. },
  184. ),
  185. );
  186. expect(controller, isNotNull);
  187. final bool canGoForwardFirstPageLoaded = await controller.canGoForward();
  188. expect(canGoForwardFirstPageLoaded, false);
  189. });
  190. testWidgets('Can go forward', (WidgetTester tester) async {
  191. WebViewController controller;
  192. await tester.pumpWidget(
  193. WebView(
  194. initialUrl: 'https://flutter.io',
  195. onWebViewCreated: (WebViewController webViewController) {
  196. controller = webViewController;
  197. },
  198. ),
  199. );
  200. expect(controller, isNotNull);
  201. await controller.loadUrl('https://youtube.com');
  202. await controller.goBack();
  203. final bool canGoForwardFirstPageBacked = await controller.canGoForward();
  204. expect(canGoForwardFirstPageBacked, true);
  205. });
  206. testWidgets('Go back', (WidgetTester tester) async {
  207. WebViewController controller;
  208. await tester.pumpWidget(
  209. WebView(
  210. initialUrl: 'https://youtube.com',
  211. onWebViewCreated: (WebViewController webViewController) {
  212. controller = webViewController;
  213. },
  214. ),
  215. );
  216. expect(controller, isNotNull);
  217. expect(await controller.currentUrl(), 'https://youtube.com');
  218. await controller.loadUrl('https://flutter.io');
  219. expect(await controller.currentUrl(), 'https://flutter.io');
  220. await controller.goBack();
  221. expect(await controller.currentUrl(), 'https://youtube.com');
  222. });
  223. testWidgets('Go forward', (WidgetTester tester) async {
  224. WebViewController controller;
  225. await tester.pumpWidget(
  226. WebView(
  227. initialUrl: 'https://youtube.com',
  228. onWebViewCreated: (WebViewController webViewController) {
  229. controller = webViewController;
  230. },
  231. ),
  232. );
  233. expect(controller, isNotNull);
  234. expect(await controller.currentUrl(), 'https://youtube.com');
  235. await controller.loadUrl('https://flutter.io');
  236. expect(await controller.currentUrl(), 'https://flutter.io');
  237. await controller.goBack();
  238. expect(await controller.currentUrl(), 'https://youtube.com');
  239. await controller.goForward();
  240. expect(await controller.currentUrl(), 'https://flutter.io');
  241. });
  242. testWidgets('Current URL', (WidgetTester tester) async {
  243. WebViewController controller;
  244. await tester.pumpWidget(
  245. WebView(
  246. onWebViewCreated: (WebViewController webViewController) {
  247. controller = webViewController;
  248. },
  249. ),
  250. );
  251. expect(controller, isNotNull);
  252. // Test a WebView without an explicitly set first URL.
  253. expect(await controller.currentUrl(), isNull);
  254. await controller.loadUrl('https://youtube.com');
  255. expect(await controller.currentUrl(), 'https://youtube.com');
  256. await controller.loadUrl('https://flutter.io');
  257. expect(await controller.currentUrl(), 'https://flutter.io');
  258. await controller.goBack();
  259. expect(await controller.currentUrl(), 'https://youtube.com');
  260. });
  261. testWidgets('Reload url', (WidgetTester tester) async {
  262. WebViewController controller;
  263. await tester.pumpWidget(
  264. WebView(
  265. initialUrl: 'https://flutter.io',
  266. onWebViewCreated: (WebViewController webViewController) {
  267. controller = webViewController;
  268. },
  269. ),
  270. );
  271. final FakePlatformWebView platformWebView =
  272. fakePlatformViewsController.lastCreatedView;
  273. expect(platformWebView.currentUrl, 'https://flutter.io');
  274. expect(platformWebView.amountOfReloadsOnCurrentUrl, 0);
  275. await controller.reload();
  276. expect(platformWebView.currentUrl, 'https://flutter.io');
  277. expect(platformWebView.amountOfReloadsOnCurrentUrl, 1);
  278. await controller.loadUrl('https://youtube.com');
  279. expect(platformWebView.amountOfReloadsOnCurrentUrl, 0);
  280. });
  281. testWidgets('evaluate Javascript', (WidgetTester tester) async {
  282. WebViewController controller;
  283. await tester.pumpWidget(
  284. WebView(
  285. initialUrl: 'https://flutter.io',
  286. javascriptMode: JavascriptMode.unrestricted,
  287. onWebViewCreated: (WebViewController webViewController) {
  288. controller = webViewController;
  289. },
  290. ),
  291. );
  292. expect(
  293. await controller.evaluateJavascript("fake js string"), "fake js string",
  294. reason: 'should get the argument');
  295. expect(
  296. () => controller.evaluateJavascript(null),
  297. throwsA(anything),
  298. );
  299. });
  300. testWidgets('evaluate Javascript with JavascriptMode disabled',
  301. (WidgetTester tester) async {
  302. WebViewController controller;
  303. await tester.pumpWidget(
  304. WebView(
  305. initialUrl: 'https://flutter.io',
  306. javascriptMode: JavascriptMode.disabled,
  307. onWebViewCreated: (WebViewController webViewController) {
  308. controller = webViewController;
  309. },
  310. ),
  311. );
  312. expect(
  313. () => controller.evaluateJavascript('fake js string'),
  314. throwsA(anything),
  315. );
  316. expect(
  317. () => controller.evaluateJavascript(null),
  318. throwsA(anything),
  319. );
  320. });
  321. testWidgets('Cookies can be cleared once', (WidgetTester tester) async {
  322. await tester.pumpWidget(
  323. const WebView(
  324. initialUrl: 'https://flutter.io',
  325. ),
  326. );
  327. final CookieManager cookieManager = CookieManager();
  328. final bool hasCookies = await cookieManager.clearCookies();
  329. expect(hasCookies, true);
  330. });
  331. testWidgets('Second cookie clear does not have cookies',
  332. (WidgetTester tester) async {
  333. await tester.pumpWidget(
  334. const WebView(
  335. initialUrl: 'https://flutter.io',
  336. ),
  337. );
  338. final CookieManager cookieManager = CookieManager();
  339. final bool hasCookies = await cookieManager.clearCookies();
  340. expect(hasCookies, true);
  341. final bool hasCookiesSecond = await cookieManager.clearCookies();
  342. expect(hasCookiesSecond, false);
  343. });
  344. testWidgets('Initial JavaScript channels', (WidgetTester tester) async {
  345. await tester.pumpWidget(
  346. WebView(
  347. initialUrl: 'https://youtube.com',
  348. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  349. // ignore: prefer_collection_literals
  350. javascriptChannels: <JavascriptChannel>[
  351. JavascriptChannel(
  352. name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
  353. JavascriptChannel(
  354. name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
  355. ].toSet(),
  356. ),
  357. );
  358. final FakePlatformWebView platformWebView =
  359. fakePlatformViewsController.lastCreatedView;
  360. expect(platformWebView.javascriptChannelNames,
  361. unorderedEquals(<String>['Tts', 'Alarm']));
  362. });
  363. test('Only valid JavaScript channel names are allowed', () {
  364. final JavascriptMessageHandler noOp = (JavascriptMessage msg) {};
  365. JavascriptChannel(name: 'Tts1', onMessageReceived: noOp);
  366. JavascriptChannel(name: '_Alarm', onMessageReceived: noOp);
  367. JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp);
  368. VoidCallback createChannel(String name) {
  369. return () {
  370. JavascriptChannel(name: name, onMessageReceived: noOp);
  371. };
  372. }
  373. expect(createChannel('1Alarm'), throwsAssertionError);
  374. expect(createChannel('foo.bar'), throwsAssertionError);
  375. expect(createChannel(''), throwsAssertionError);
  376. });
  377. testWidgets('Unique JavaScript channel names are required',
  378. (WidgetTester tester) async {
  379. await tester.pumpWidget(
  380. WebView(
  381. initialUrl: 'https://youtube.com',
  382. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  383. // ignore: prefer_collection_literals
  384. javascriptChannels: <JavascriptChannel>[
  385. JavascriptChannel(
  386. name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
  387. JavascriptChannel(
  388. name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
  389. ].toSet(),
  390. ),
  391. );
  392. expect(tester.takeException(), isNot(null));
  393. });
  394. testWidgets('JavaScript channels update', (WidgetTester tester) async {
  395. await tester.pumpWidget(
  396. WebView(
  397. initialUrl: 'https://youtube.com',
  398. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  399. // ignore: prefer_collection_literals
  400. javascriptChannels: <JavascriptChannel>[
  401. JavascriptChannel(
  402. name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
  403. JavascriptChannel(
  404. name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
  405. ].toSet(),
  406. ),
  407. );
  408. await tester.pumpWidget(
  409. WebView(
  410. initialUrl: 'https://youtube.com',
  411. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  412. // ignore: prefer_collection_literals
  413. javascriptChannels: <JavascriptChannel>[
  414. JavascriptChannel(
  415. name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
  416. JavascriptChannel(
  417. name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}),
  418. JavascriptChannel(
  419. name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}),
  420. ].toSet(),
  421. ),
  422. );
  423. final FakePlatformWebView platformWebView =
  424. fakePlatformViewsController.lastCreatedView;
  425. expect(platformWebView.javascriptChannelNames,
  426. unorderedEquals(<String>['Tts', 'Alarm2', 'Alarm3']));
  427. });
  428. testWidgets('Remove all JavaScript channels and then add',
  429. (WidgetTester tester) async {
  430. // This covers a specific bug we had where after updating javascriptChannels to null,
  431. // updating it again with a subset of the previously registered channels fails as the
  432. // widget's cache of current channel wasn't properly updated when updating javascriptChannels to
  433. // null.
  434. await tester.pumpWidget(
  435. WebView(
  436. initialUrl: 'https://youtube.com',
  437. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  438. // ignore: prefer_collection_literals
  439. javascriptChannels: <JavascriptChannel>[
  440. JavascriptChannel(
  441. name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
  442. ].toSet(),
  443. ),
  444. );
  445. await tester.pumpWidget(
  446. const WebView(
  447. initialUrl: 'https://youtube.com',
  448. ),
  449. );
  450. await tester.pumpWidget(
  451. WebView(
  452. initialUrl: 'https://youtube.com',
  453. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  454. // ignore: prefer_collection_literals
  455. javascriptChannels: <JavascriptChannel>[
  456. JavascriptChannel(
  457. name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
  458. ].toSet(),
  459. ),
  460. );
  461. final FakePlatformWebView platformWebView =
  462. fakePlatformViewsController.lastCreatedView;
  463. expect(platformWebView.javascriptChannelNames,
  464. unorderedEquals(<String>['Tts']));
  465. });
  466. testWidgets('JavaScript channel messages', (WidgetTester tester) async {
  467. final List<String> ttsMessagesReceived = <String>[];
  468. final List<String> alarmMessagesReceived = <String>[];
  469. await tester.pumpWidget(
  470. WebView(
  471. initialUrl: 'https://youtube.com',
  472. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  473. // ignore: prefer_collection_literals
  474. javascriptChannels: <JavascriptChannel>[
  475. JavascriptChannel(
  476. name: 'Tts',
  477. onMessageReceived: (JavascriptMessage msg) {
  478. ttsMessagesReceived.add(msg.message);
  479. }),
  480. JavascriptChannel(
  481. name: 'Alarm',
  482. onMessageReceived: (JavascriptMessage msg) {
  483. alarmMessagesReceived.add(msg.message);
  484. }),
  485. ].toSet(),
  486. ),
  487. );
  488. final FakePlatformWebView platformWebView =
  489. fakePlatformViewsController.lastCreatedView;
  490. expect(ttsMessagesReceived, isEmpty);
  491. expect(alarmMessagesReceived, isEmpty);
  492. platformWebView.fakeJavascriptPostMessage('Tts', 'Hello');
  493. platformWebView.fakeJavascriptPostMessage('Tts', 'World');
  494. expect(ttsMessagesReceived, <String>['Hello', 'World']);
  495. });
  496. group('$PageStartedCallback', () {
  497. testWidgets('onPageStarted is not null', (WidgetTester tester) async {
  498. String returnedUrl;
  499. await tester.pumpWidget(WebView(
  500. initialUrl: 'https://youtube.com',
  501. onPageStarted: (String url) {
  502. returnedUrl = url;
  503. },
  504. ));
  505. final FakePlatformWebView platformWebView =
  506. fakePlatformViewsController.lastCreatedView;
  507. platformWebView.fakeOnPageStartedCallback();
  508. expect(platformWebView.currentUrl, returnedUrl);
  509. });
  510. testWidgets('onPageStarted is null', (WidgetTester tester) async {
  511. await tester.pumpWidget(const WebView(
  512. initialUrl: 'https://youtube.com',
  513. onPageStarted: null,
  514. ));
  515. final FakePlatformWebView platformWebView =
  516. fakePlatformViewsController.lastCreatedView;
  517. // The platform side will always invoke a call for onPageStarted. This is
  518. // to test that it does not crash on a null callback.
  519. platformWebView.fakeOnPageStartedCallback();
  520. });
  521. testWidgets('onPageStarted changed', (WidgetTester tester) async {
  522. String returnedUrl;
  523. await tester.pumpWidget(WebView(
  524. initialUrl: 'https://youtube.com',
  525. onPageStarted: (String url) {},
  526. ));
  527. await tester.pumpWidget(WebView(
  528. initialUrl: 'https://youtube.com',
  529. onPageStarted: (String url) {
  530. returnedUrl = url;
  531. },
  532. ));
  533. final FakePlatformWebView platformWebView =
  534. fakePlatformViewsController.lastCreatedView;
  535. platformWebView.fakeOnPageStartedCallback();
  536. expect(platformWebView.currentUrl, returnedUrl);
  537. });
  538. });
  539. group('$PageFinishedCallback', () {
  540. testWidgets('onPageFinished is not null', (WidgetTester tester) async {
  541. String returnedUrl;
  542. await tester.pumpWidget(WebView(
  543. initialUrl: 'https://youtube.com',
  544. onPageFinished: (String url) {
  545. returnedUrl = url;
  546. },
  547. ));
  548. final FakePlatformWebView platformWebView =
  549. fakePlatformViewsController.lastCreatedView;
  550. platformWebView.fakeOnPageFinishedCallback();
  551. expect(platformWebView.currentUrl, returnedUrl);
  552. });
  553. testWidgets('onPageFinished is null', (WidgetTester tester) async {
  554. await tester.pumpWidget(const WebView(
  555. initialUrl: 'https://youtube.com',
  556. onPageFinished: null,
  557. ));
  558. final FakePlatformWebView platformWebView =
  559. fakePlatformViewsController.lastCreatedView;
  560. // The platform side will always invoke a call for onPageFinished. This is
  561. // to test that it does not crash on a null callback.
  562. platformWebView.fakeOnPageFinishedCallback();
  563. });
  564. testWidgets('onPageFinished changed', (WidgetTester tester) async {
  565. String returnedUrl;
  566. await tester.pumpWidget(WebView(
  567. initialUrl: 'https://youtube.com',
  568. onPageFinished: (String url) {},
  569. ));
  570. await tester.pumpWidget(WebView(
  571. initialUrl: 'https://youtube.com',
  572. onPageFinished: (String url) {
  573. returnedUrl = url;
  574. },
  575. ));
  576. final FakePlatformWebView platformWebView =
  577. fakePlatformViewsController.lastCreatedView;
  578. platformWebView.fakeOnPageFinishedCallback();
  579. expect(platformWebView.currentUrl, returnedUrl);
  580. });
  581. });
  582. group('navigationDelegate', () {
  583. testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
  584. await tester.pumpWidget(const WebView(
  585. initialUrl: 'https://youtube.com',
  586. ));
  587. final FakePlatformWebView platformWebView =
  588. fakePlatformViewsController.lastCreatedView;
  589. expect(platformWebView.hasNavigationDelegate, false);
  590. await tester.pumpWidget(WebView(
  591. initialUrl: 'https://youtube.com',
  592. navigationDelegate: (NavigationRequest r) => null,
  593. ));
  594. expect(platformWebView.hasNavigationDelegate, true);
  595. });
  596. testWidgets('Block navigation', (WidgetTester tester) async {
  597. final List<NavigationRequest> navigationRequests = <NavigationRequest>[];
  598. await tester.pumpWidget(WebView(
  599. initialUrl: 'https://youtube.com',
  600. navigationDelegate: (NavigationRequest request) {
  601. navigationRequests.add(request);
  602. // Only allow navigating to https://flutter.dev
  603. return request.url == 'https://flutter.dev'
  604. ? NavigationDecision.navigate
  605. : NavigationDecision.prevent;
  606. }));
  607. final FakePlatformWebView platformWebView =
  608. fakePlatformViewsController.lastCreatedView;
  609. expect(platformWebView.hasNavigationDelegate, true);
  610. platformWebView.fakeNavigate('https://www.google.com');
  611. // The navigation delegate only allows navigation to https://flutter.dev
  612. // so we should still be in https://youtube.com.
  613. expect(platformWebView.currentUrl, 'https://youtube.com');
  614. expect(navigationRequests.length, 1);
  615. expect(navigationRequests[0].url, 'https://www.google.com');
  616. expect(navigationRequests[0].isForMainFrame, true);
  617. platformWebView.fakeNavigate('https://flutter.dev');
  618. await tester.pump();
  619. expect(platformWebView.currentUrl, 'https://flutter.dev');
  620. });
  621. });
  622. group('debuggingEnabled', () {
  623. testWidgets('enable debugging', (WidgetTester tester) async {
  624. await tester.pumpWidget(const WebView(
  625. debuggingEnabled: true,
  626. ));
  627. final FakePlatformWebView platformWebView =
  628. fakePlatformViewsController.lastCreatedView;
  629. expect(platformWebView.debuggingEnabled, true);
  630. });
  631. testWidgets('defaults to false', (WidgetTester tester) async {
  632. await tester.pumpWidget(const WebView());
  633. final FakePlatformWebView platformWebView =
  634. fakePlatformViewsController.lastCreatedView;
  635. expect(platformWebView.debuggingEnabled, false);
  636. });
  637. testWidgets('can be changed', (WidgetTester tester) async {
  638. final GlobalKey key = GlobalKey();
  639. await tester.pumpWidget(WebView(key: key));
  640. final FakePlatformWebView platformWebView =
  641. fakePlatformViewsController.lastCreatedView;
  642. await tester.pumpWidget(WebView(
  643. key: key,
  644. debuggingEnabled: true,
  645. ));
  646. expect(platformWebView.debuggingEnabled, true);
  647. await tester.pumpWidget(WebView(
  648. key: key,
  649. debuggingEnabled: false,
  650. ));
  651. expect(platformWebView.debuggingEnabled, false);
  652. });
  653. });
  654. group('Custom platform implementation', () {
  655. setUpAll(() {
  656. WebView.platform = MyWebViewPlatform();
  657. });
  658. tearDownAll(() {
  659. WebView.platform = null;
  660. });
  661. testWidgets('creation', (WidgetTester tester) async {
  662. await tester.pumpWidget(
  663. const WebView(
  664. initialUrl: 'https://youtube.com',
  665. gestureNavigationEnabled: true,
  666. ),
  667. );
  668. final MyWebViewPlatform builder = WebView.platform;
  669. final MyWebViewPlatformController platform = builder.lastPlatformBuilt;
  670. expect(
  671. platform.creationParams,
  672. MatchesCreationParams(CreationParams(
  673. initialUrl: 'https://youtube.com',
  674. webSettings: WebSettings(
  675. javascriptMode: JavascriptMode.disabled,
  676. hasNavigationDelegate: false,
  677. debuggingEnabled: false,
  678. userAgent: WebSetting<String>.of(null),
  679. gestureNavigationEnabled: true,
  680. ),
  681. // TODO(iskakaushik): Remove this when collection literals makes it to stable.
  682. // ignore: prefer_collection_literals
  683. javascriptChannelNames: Set<String>(),
  684. )));
  685. });
  686. testWidgets('loadUrl', (WidgetTester tester) async {
  687. WebViewController controller;
  688. await tester.pumpWidget(
  689. WebView(
  690. initialUrl: 'https://youtube.com',
  691. onWebViewCreated: (WebViewController webViewController) {
  692. controller = webViewController;
  693. },
  694. ),
  695. );
  696. final MyWebViewPlatform builder = WebView.platform;
  697. final MyWebViewPlatformController platform = builder.lastPlatformBuilt;
  698. final Map<String, String> headers = <String, String>{
  699. 'header': 'value',
  700. };
  701. await controller.loadUrl('https://google.com', headers: headers);
  702. expect(platform.lastUrlLoaded, 'https://google.com');
  703. expect(platform.lastRequestHeaders, headers);
  704. });
  705. });
  706. testWidgets('Set UserAgent', (WidgetTester tester) async {
  707. await tester.pumpWidget(const WebView(
  708. initialUrl: 'https://youtube.com',
  709. javascriptMode: JavascriptMode.unrestricted,
  710. ));
  711. final FakePlatformWebView platformWebView =
  712. fakePlatformViewsController.lastCreatedView;
  713. expect(platformWebView.userAgent, isNull);
  714. await tester.pumpWidget(const WebView(
  715. initialUrl: 'https://youtube.com',
  716. javascriptMode: JavascriptMode.unrestricted,
  717. userAgent: 'UA',
  718. ));
  719. expect(platformWebView.userAgent, 'UA');
  720. });
  721. }
  722. class FakePlatformWebView {
  723. FakePlatformWebView(int id, Map<dynamic, dynamic> params) {
  724. if (params.containsKey('initialUrl')) {
  725. final String initialUrl = params['initialUrl'];
  726. if (initialUrl != null) {
  727. history.add(initialUrl);
  728. currentPosition++;
  729. }
  730. }
  731. if (params.containsKey('javascriptChannelNames')) {
  732. javascriptChannelNames =
  733. List<String>.from(params['javascriptChannelNames']);
  734. }
  735. javascriptMode = JavascriptMode.values[params['settings']['jsMode']];
  736. hasNavigationDelegate =
  737. params['settings']['hasNavigationDelegate'] ?? false;
  738. debuggingEnabled = params['settings']['debuggingEnabled'];
  739. userAgent = params['settings']['userAgent'];
  740. channel = MethodChannel(
  741. 'plugins.flutter.io/webview_$id', const StandardMethodCodec());
  742. channel.setMockMethodCallHandler(onMethodCall);
  743. }
  744. MethodChannel channel;
  745. List<String> history = <String>[];
  746. int currentPosition = -1;
  747. int amountOfReloadsOnCurrentUrl = 0;
  748. bool hasCache = true;
  749. String get currentUrl => history.isEmpty ? null : history[currentPosition];
  750. JavascriptMode javascriptMode;
  751. List<String> javascriptChannelNames;
  752. bool hasNavigationDelegate;
  753. bool debuggingEnabled;
  754. String userAgent;
  755. Future<dynamic> onMethodCall(MethodCall call) {
  756. switch (call.method) {
  757. case 'loadUrl':
  758. final Map<dynamic, dynamic> request = call.arguments;
  759. _loadUrl(request['url']);
  760. return Future<void>.sync(() {});
  761. case 'updateSettings':
  762. if (call.arguments['jsMode'] != null) {
  763. javascriptMode = JavascriptMode.values[call.arguments['jsMode']];
  764. }
  765. if (call.arguments['hasNavigationDelegate'] != null) {
  766. hasNavigationDelegate = call.arguments['hasNavigationDelegate'];
  767. }
  768. if (call.arguments['debuggingEnabled'] != null) {
  769. debuggingEnabled = call.arguments['debuggingEnabled'];
  770. }
  771. userAgent = call.arguments['userAgent'];
  772. break;
  773. case 'canGoBack':
  774. return Future<bool>.sync(() => currentPosition > 0);
  775. break;
  776. case 'canGoForward':
  777. return Future<bool>.sync(() => currentPosition < history.length - 1);
  778. break;
  779. case 'goBack':
  780. currentPosition = max(-1, currentPosition - 1);
  781. return Future<void>.sync(() {});
  782. break;
  783. case 'goForward':
  784. currentPosition = min(history.length - 1, currentPosition + 1);
  785. return Future<void>.sync(() {});
  786. case 'reload':
  787. amountOfReloadsOnCurrentUrl++;
  788. return Future<void>.sync(() {});
  789. break;
  790. case 'currentUrl':
  791. return Future<String>.value(currentUrl);
  792. break;
  793. case 'evaluateJavascript':
  794. return Future<dynamic>.value(call.arguments);
  795. break;
  796. case 'addJavascriptChannels':
  797. final List<String> channelNames = List<String>.from(call.arguments);
  798. javascriptChannelNames.addAll(channelNames);
  799. break;
  800. case 'removeJavascriptChannels':
  801. final List<String> channelNames = List<String>.from(call.arguments);
  802. javascriptChannelNames
  803. .removeWhere((String channel) => channelNames.contains(channel));
  804. break;
  805. case 'clearCache':
  806. hasCache = false;
  807. return Future<void>.sync(() {});
  808. }
  809. return Future<void>.sync(() {});
  810. }
  811. void fakeJavascriptPostMessage(String jsChannel, String message) {
  812. final StandardMethodCodec codec = const StandardMethodCodec();
  813. final Map<String, dynamic> arguments = <String, dynamic>{
  814. 'channel': jsChannel,
  815. 'message': message
  816. };
  817. final ByteData data = codec
  818. .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments));
  819. ServicesBinding.instance.defaultBinaryMessenger
  820. .handlePlatformMessage(channel.name, data, (ByteData data) {});
  821. }
  822. // Fakes a main frame navigation that was initiated by the webview, e.g when
  823. // the user clicks a link in the currently loaded page.
  824. void fakeNavigate(String url) {
  825. if (!hasNavigationDelegate) {
  826. print('no navigation delegate');
  827. _loadUrl(url);
  828. return;
  829. }
  830. final StandardMethodCodec codec = const StandardMethodCodec();
  831. final Map<String, dynamic> arguments = <String, dynamic>{
  832. 'url': url,
  833. 'isForMainFrame': true
  834. };
  835. final ByteData data =
  836. codec.encodeMethodCall(MethodCall('navigationRequest', arguments));
  837. ServicesBinding.instance.defaultBinaryMessenger
  838. .handlePlatformMessage(channel.name, data, (ByteData data) {
  839. final bool allow = codec.decodeEnvelope(data);
  840. if (allow) {
  841. _loadUrl(url);
  842. }
  843. });
  844. }
  845. void fakeOnPageStartedCallback() {
  846. final StandardMethodCodec codec = const StandardMethodCodec();
  847. final ByteData data = codec.encodeMethodCall(MethodCall(
  848. 'onPageStarted',
  849. <dynamic, dynamic>{'url': currentUrl},
  850. ));
  851. ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
  852. channel.name,
  853. data,
  854. (ByteData data) {},
  855. );
  856. }
  857. void fakeOnPageFinishedCallback() {
  858. final StandardMethodCodec codec = const StandardMethodCodec();
  859. final ByteData data = codec.encodeMethodCall(MethodCall(
  860. 'onPageFinished',
  861. <dynamic, dynamic>{'url': currentUrl},
  862. ));
  863. ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
  864. channel.name,
  865. data,
  866. (ByteData data) {},
  867. );
  868. }
  869. void _loadUrl(String url) {
  870. history = history.sublist(0, currentPosition + 1);
  871. history.add(url);
  872. currentPosition++;
  873. amountOfReloadsOnCurrentUrl = 0;
  874. }
  875. }
  876. class _FakePlatformViewsController {
  877. FakePlatformWebView lastCreatedView;
  878. Future<dynamic> fakePlatformViewsMethodHandler(MethodCall call) {
  879. switch (call.method) {
  880. case 'create':
  881. final Map<dynamic, dynamic> args = call.arguments;
  882. final Map<dynamic, dynamic> params = _decodeParams(args['params']);
  883. lastCreatedView = FakePlatformWebView(
  884. args['id'],
  885. params,
  886. );
  887. return Future<int>.sync(() => 1);
  888. default:
  889. return Future<void>.sync(() {});
  890. }
  891. }
  892. void reset() {
  893. lastCreatedView = null;
  894. }
  895. }
  896. Map<dynamic, dynamic> _decodeParams(Uint8List paramsMessage) {
  897. final ByteBuffer buffer = paramsMessage.buffer;
  898. final ByteData messageBytes = buffer.asByteData(
  899. paramsMessage.offsetInBytes,
  900. paramsMessage.lengthInBytes,
  901. );
  902. return const StandardMessageCodec().decodeMessage(messageBytes);
  903. }
  904. class _FakeCookieManager {
  905. _FakeCookieManager() {
  906. final MethodChannel channel = const MethodChannel(
  907. 'plugins.flutter.io/cookie_manager',
  908. StandardMethodCodec(),
  909. );
  910. channel.setMockMethodCallHandler(onMethodCall);
  911. }
  912. bool hasCookies = true;
  913. Future<bool> onMethodCall(MethodCall call) {
  914. switch (call.method) {
  915. case 'clearCookies':
  916. bool hadCookies = false;
  917. if (hasCookies) {
  918. hadCookies = true;
  919. hasCookies = false;
  920. }
  921. return Future<bool>.sync(() {
  922. return hadCookies;
  923. });
  924. break;
  925. }
  926. return Future<bool>.sync(() => null);
  927. }
  928. void reset() {
  929. hasCookies = true;
  930. }
  931. }
  932. class MyWebViewPlatform implements WebViewPlatform {
  933. MyWebViewPlatformController lastPlatformBuilt;
  934. @override
  935. Widget build({
  936. BuildContext context,
  937. CreationParams creationParams,
  938. @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
  939. @required WebViewPlatformCreatedCallback onWebViewPlatformCreated,
  940. Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
  941. }) {
  942. assert(onWebViewPlatformCreated != null);
  943. lastPlatformBuilt = MyWebViewPlatformController(
  944. creationParams, gestureRecognizers, webViewPlatformCallbacksHandler);
  945. onWebViewPlatformCreated(lastPlatformBuilt);
  946. return Container();
  947. }
  948. @override
  949. Future<bool> clearCookies() {
  950. return Future<bool>.sync(() => null);
  951. }
  952. }
  953. class MyWebViewPlatformController extends WebViewPlatformController {
  954. MyWebViewPlatformController(this.creationParams, this.gestureRecognizers,
  955. WebViewPlatformCallbacksHandler platformHandler)
  956. : super(platformHandler);
  957. CreationParams creationParams;
  958. Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
  959. String lastUrlLoaded;
  960. Map<String, String> lastRequestHeaders;
  961. @override
  962. Future<void> loadUrl(String url, Map<String, String> headers) {
  963. equals(1, 1);
  964. lastUrlLoaded = url;
  965. lastRequestHeaders = headers;
  966. return null;
  967. }
  968. }
  969. class MatchesWebSettings extends Matcher {
  970. MatchesWebSettings(this._webSettings);
  971. final WebSettings _webSettings;
  972. @override
  973. Description describe(Description description) =>
  974. description.add('$_webSettings');
  975. @override
  976. bool matches(
  977. covariant WebSettings webSettings, Map<dynamic, dynamic> matchState) {
  978. return _webSettings.javascriptMode == webSettings.javascriptMode &&
  979. _webSettings.hasNavigationDelegate ==
  980. webSettings.hasNavigationDelegate &&
  981. _webSettings.debuggingEnabled == webSettings.debuggingEnabled &&
  982. _webSettings.gestureNavigationEnabled ==
  983. webSettings.gestureNavigationEnabled &&
  984. _webSettings.userAgent == webSettings.userAgent;
  985. }
  986. }
  987. class MatchesCreationParams extends Matcher {
  988. MatchesCreationParams(this._creationParams);
  989. final CreationParams _creationParams;
  990. @override
  991. Description describe(Description description) =>
  992. description.add('$_creationParams');
  993. @override
  994. bool matches(covariant CreationParams creationParams,
  995. Map<dynamic, dynamic> matchState) {
  996. return _creationParams.initialUrl == creationParams.initialUrl &&
  997. MatchesWebSettings(_creationParams.webSettings)
  998. .matches(creationParams.webSettings, matchState) &&
  999. orderedEquals(_creationParams.javascriptChannelNames)
  1000. .matches(creationParams.javascriptChannelNames, matchState);
  1001. }
  1002. }