Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[webview_flutter] Implementations of loadFile and loadHtmlString for WKWebView #4486

Merged
merged 8 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.3.0

* Implemented new `loadFile` and `loadHtmlString` methods from the platform interface.

## 2.2.0

* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 50;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -274,7 +274,7 @@
isa = PBXProject;
attributes = {
DefaultBuildSystemTypeForWorkspace = Original;
LastUpgradeCheck = 1030;
LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
68BDCAE823C3F7CB00D9C032 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1030"
LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,201 @@ - (void)testContentInsetsSumAlwaysZeroAfterSetFrame {
}
}

- (void)testLoadFileSucceeds {
NSString *testFilePath = @"/assets/file.html";
NSURL *url = [NSURL fileURLWithPath:testFilePath isDirectory:NO];
XCTestExpectation *resultExpectation =
[self expectationWithDescription:@"Should return successful result over the method channel."];
FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
controller.webView = mockWebView;
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile"
arguments:testFilePath]
result:^(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];

[self waitForExpectations:@[ resultExpectation ] timeout:30.0];
OCMVerify([mockWebView loadFileURL:url
allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]);
}

- (void)testLoadFileFailsWithInvalidPath {
NSArray *resultExpectations = @[
[self expectationWithDescription:@"Should return failed result when argument is nil."],
[self expectationWithDescription:
@"Should return failed result when argument is not of type NSString*."],
[self expectationWithDescription:
@"Should return failed result when argument is an empty string."],
];

FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
controller.webView = mockWebView;
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile" arguments:nil]
result:^(id _Nullable result) {
FlutterError *expected =
[FlutterError errorWithCode:@"loadFile_failed"
message:@"Failed parsing file path."
details:@"Argument is nil."];
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[0] fulfill];
}];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile" arguments:@(10)]
result:^(id _Nullable result) {
FlutterError *expected =
[FlutterError errorWithCode:@"loadFile_failed"
message:@"Failed parsing file path."
details:@"Argument is not of type NSString."];
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[1] fulfill];
}];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile" arguments:@""]
result:^(id _Nullable result) {
FlutterError *expected =
[FlutterError errorWithCode:@"loadFile_failed"
message:@"Failed parsing file path."
details:@"Argument contains an empty string."];
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[2] fulfill];
}];

[self waitForExpectations:resultExpectations timeout:1.0];
OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]);
}

- (void)testLoadFileSucceedsWithBaseUrl {
NSURL *baseUrl = [NSURL URLWithString:@"https://flutter.dev"];
XCTestExpectation *resultExpectation =
[self expectationWithDescription:@"Should return successful result over the method channel."];
FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
controller.webView = mockWebView;
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:@{
@"html" : @"some HTML string",
@"baseUrl" : @"https://flutter.dev"
}]
result:^(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];

[self waitForExpectations:@[ resultExpectation ] timeout:30.0];
OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:baseUrl]);
}

- (void)testLoadFileSucceedsWithoutBaseUrl {
XCTestExpectation *resultExpectation =
[self expectationWithDescription:@"Should return successful result over the method channel."];
FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
controller.webView = mockWebView;
[controller
onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:@{@"html" : @"some HTML string"}]
result:^(id _Nullable result) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];

[self waitForExpectations:@[ resultExpectation ] timeout:30.0];
OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:nil]);
}

- (void)testLoadHtmlStringFailsWithInvalidArgument {
NSArray *resultExpectations = @[
[self expectationWithDescription:@"Should return failed result when argument is nil."],
[self expectationWithDescription:
@"Should return failed result when argument is not of type NSDictionary*."],
[self expectationWithDescription:@"Should return failed result when HTML argument is nil."],
[self expectationWithDescription:
@"Should return failed result when HTML argument is not of type NSString*."],
[self expectationWithDescription:
@"Should return failed result when HTML argument is an empty string."],
];

FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
controller.webView = mockWebView;
FlutterError *expected = [FlutterError
errorWithCode:@"loadHtmlString_failed"
message:@"Failed parsing arguments."
details:@"Arguments should be a dictionary containing at least a 'html' element and "
@"optionally a 'baseUrl' argument. For example: `@{ @\"html\": @\"some html "
@"code\", @\"baseUrl\": @\"https://flutter.dev\" }`"];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:nil]
result:^(id _Nullable result) {
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[0] fulfill];
}];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:@""]
result:^(id _Nullable result) {
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[1] fulfill];
}];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:@{}]
result:^(id _Nullable result) {
FlutterError *expected =
[FlutterError errorWithCode:@"loadHtmlString_failed"
message:@"Failed parsing HTML string argument."
details:@"Argument is nil."];
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[2] fulfill];
}];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:@{
@"html" : @(42),
}]
result:^(id _Nullable result) {
FlutterError *expected =
[FlutterError errorWithCode:@"loadHtmlString_failed"
message:@"Failed parsing HTML string argument."
details:@"Argument is not of type NSString."];
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[3] fulfill];
}];
[controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
arguments:@{
@"html" : @"",
}]
result:^(id _Nullable result) {
FlutterError *expected =
[FlutterError errorWithCode:@"loadHtmlString_failed"
message:@"Failed parsing HTML string argument."
details:@"Argument contains an empty string."];
[FLTWebViewTests assertFlutterError:result withExpected:expected];
[resultExpectations[4] fulfill];
}];

[self waitForExpectations:resultExpectations timeout:1.0];
OCMReject([mockWebView loadHTMLString:[OCMArg any] baseURL:[OCMArg any]]);
}

- (void)testRunJavascriptFailsForNullString {
// Setup
FLTWebViewController *controller =
Expand Down Expand Up @@ -301,4 +496,12 @@ - (void)testRunJavascriptReturningResultReturnsErrorResultForWKError {
[self waitForExpectationsWithTimeout:30.0 handler:nil];
}

+ (void)assertFlutterError:(id)actual withExpected:(FlutterError *)expected {
XCTAssertTrue([actual class] == [FlutterError class]);
FlutterError *errorResult = actual;
XCTAssertEqualObjects(errorResult.code, expected.code);
XCTAssertEqualObjects(errorResult.message, expected.message);
XCTAssertEqualObjects(errorResult.details, expected.details);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

import 'navigation_decision.dart';
Expand All @@ -33,6 +36,21 @@ The navigation delegate is set to block navigation to the youtube website.
</html>
''';

const String kLocalFileExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body>

<h1>Local demo page</h1>
<p>This is an example page used to demonstrate how to load a local file or HTML string using the <a href="https://pub.dev/packages/webview_flutter">Flutter webview</a> plugin.</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: There's no reason not to break this line to wrap to 80 characters, since HTML doesn't care about whitespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


</body>
</html>
''';

class _WebViewExample extends StatefulWidget {
const _WebViewExample({Key? key}) : super(key: key);

Expand Down Expand Up @@ -120,6 +138,8 @@ enum _MenuOptions {
listCache,
clearCache,
navigationDelegate,
loadLocalFile,
loadHtmlString,
}

class _SampleMenu extends StatelessWidget {
Expand Down Expand Up @@ -157,6 +177,12 @@ class _SampleMenu extends StatelessWidget {
case _MenuOptions.navigationDelegate:
_onNavigationDelegateExample(controller.data!, context);
break;
case _MenuOptions.loadLocalFile:
_onLoadLocalFileExample(controller.data!, context);
break;
case _MenuOptions.loadHtmlString:
_onLoadHtmlStringExample(controller.data!, context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<_MenuOptions>>[
Expand Down Expand Up @@ -189,6 +215,14 @@ class _SampleMenu extends StatelessWidget {
value: _MenuOptions.navigationDelegate,
child: Text('Navigation Delegate example'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.loadHtmlString,
child: Text('Load HTML string'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.loadLocalFile,
child: Text('Load local file'),
),
BeMacized marked this conversation as resolved.
Show resolved Hide resolved
],
);
},
Expand Down Expand Up @@ -259,6 +293,18 @@ class _SampleMenu extends StatelessWidget {
await controller.loadUrl('data:text/html;base64,$contentBase64');
}

void _onLoadLocalFileExample(
WebViewController controller, BuildContext context) async {
String pathToIndex = await _prepareLocalFile();

await controller.loadFile(pathToIndex);
}

void _onLoadHtmlStringExample(
WebViewController controller, BuildContext context) async {
await controller.loadHtmlString(kLocalFileExamplePage);
}

Widget _getCookieList(String cookies) {
if (cookies == null || cookies == '""') {
return Container();
Expand All @@ -272,6 +318,20 @@ class _SampleMenu extends StatelessWidget {
children: cookieWidgets.toList(),
);
}

static Future<String> _prepareLocalFile() async {
final String tmpDir = (await getTemporaryDirectory()).path;
File indexFile = File('$tmpDir/www/index.html');

if (await indexFile.exists()) {
return indexFile.path;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early return seems odd; why would we want not changes someone made locally to the example HTML (e.g., to try to reproduce an issue in the context of the example) after having already run once to take effect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I added this check with the idea that if the file exists we don't have to write it again (in case the user presses the button twice in the same session). However I didn't think about the fact that the file will be persisted over multiple sessions and developers might want to change the HTML contents.

}

await Directory('$tmpDir/www').create(recursive: true);
await indexFile.writeAsString(kLocalFileExamplePage);

return indexFile.path;
}
BeMacized marked this conversation as resolved.
Show resolved Hide resolved
}

class _NavigationControls extends StatelessWidget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,35 @@ class WebViewController {

WebView _widget;

/// Loads the file located on the specified [absoluteFilePath].
///
/// The [absoluteFilePath] parameter should contain the absolute path to the
/// file as it is stored on the device. For example:
/// `/Users/username/Documents/www/index.html`.
///
/// Throws an ArgumentError if the [absoluteFilePath] does not exist.
Future<void> loadFile(
String absoluteFilePath,
) {
assert(absoluteFilePath != null || absoluteFilePath.isNotEmpty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need null assertions in example code that's null-safe; no non-null-safe code can call it since it's not a library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the redundant assert.

return _webViewPlatformController.loadFile(absoluteFilePath);
}

/// Loads the supplied HTML string.
///
/// The [baseUrl] parameter is used when resolving relative URLs within the
/// HTML string.
Future<void> loadHtmlString(
String html, {
String? baseUrl,
}) {
assert(html != null || html.isNotEmpty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the redundant assert.

return _webViewPlatformController.loadHtmlString(
html,
baseUrl: baseUrl,
);
}

/// Loads the specified URL.
///
/// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will
Expand Down
Loading