Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Implementation of loadFile for WKWebView.
Adds native implementation for the `loadFile` method channel call to the
webview_flutter_wkwebview package.
  • Loading branch information
mvanbeusekom committed Nov 8, 2021
commit c1dc83ae33a76a4dfe19ba159b471c8b1c72671e
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,104 @@ - (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)testLoadFileFailsWithNilPath {
XCTestExpectation *resultExpectation =
[self expectationWithDescription:@"Should return failed 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:nil]
result:^(id _Nullable result) {
XCTAssertTrue([result class] == [FlutterError class]);
FlutterError *errorResult = result;
XCTAssertEqualObjects(errorResult.code, @"loadFile_failed");
XCTAssertEqualObjects(errorResult.message, @"Failed parsing file path.");
XCTAssertEqualObjects(errorResult.details, @"Argument is nil.");
[resultExpectation fulfill];
}];

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

- (void)testLoadFileFailsWithNonStringPath {
XCTestExpectation *resultExpectation =
[self expectationWithDescription:@"Should return failed 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:@(10)]
result:^(id _Nullable result) {
XCTAssertTrue([result class] == [FlutterError class]);
FlutterError *errorResult = result;
XCTAssertEqualObjects(errorResult.code, @"loadFile_failed");
XCTAssertEqualObjects(errorResult.message, @"Failed parsing file path.");
XCTAssertEqualObjects(errorResult.details, @"Argument is not of type NSString.");
[resultExpectation fulfill];
}];

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

- (void)testLoadFileFailsWithEmptyPath {
XCTestExpectation *resultExpectation =
[self expectationWithDescription:@"Should return failed 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:@""]
result:^(id _Nullable result) {
XCTAssertTrue([result class] == [FlutterError class]);
FlutterError *errorResult = result;
XCTAssertEqualObjects(errorResult.code, @"loadFile_failed");
XCTAssertEqualObjects(errorResult.message, @"Failed parsing file path.");
XCTAssertEqualObjects(errorResult.details, @"Argument contains an empty string.");
[resultExpectation fulfill];
}];

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

- (void)testRunJavascriptFailsForNullString {
// Setup
FLTWebViewController *controller =
Expand Down
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>Local demo page</title>
</head>
<body>

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

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

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

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

class _SampleMenu extends StatelessWidget {
Expand Down Expand Up @@ -157,6 +176,9 @@ class _SampleMenu extends StatelessWidget {
case _MenuOptions.navigationDelegate:
_onNavigationDelegateExample(controller.data!, context);
break;
case _MenuOptions.loadLocalFile:
_onLoadLocalFileExample(controller.data!, context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<_MenuOptions>>[
Expand Down Expand Up @@ -189,6 +211,10 @@ class _SampleMenu extends StatelessWidget {
value: _MenuOptions.navigationDelegate,
child: Text('Navigation Delegate example'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.loadLocalFile,
child: Text('Load local file'),
),
],
);
},
Expand Down Expand Up @@ -259,6 +285,13 @@ 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);
}

Widget _getCookieList(String cookies) {
if (cookies == null || cookies == '""') {
return Container();
Expand All @@ -272,6 +305,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;
}
}

class _NavigationControls extends StatelessWidget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,20 @@ 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 specified URL.
///
/// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ environment:
dependencies:
flutter:
sdk: flutter

path_provider: ^2.0.6

webview_flutter_wkwebview:
# When depending on this package from a real application you should use:
# webview_flutter: ^x.y.z
Expand All @@ -31,3 +34,4 @@ flutter:
assets:
- assets/sample_audio.ogg
- assets/sample_video.mp4

Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ - (UIView*)view {
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([[call method] isEqualToString:@"updateSettings"]) {
[self onUpdateSettings:call result:result];
} else if ([[call method] isEqualToString:@"loadFile"]) {
[self onLoadFile:call result:result];
} else if ([[call method] isEqualToString:@"loadUrl"]) {
[self onLoadUrl:call result:result];
} else if ([[call method] isEqualToString:@"canGoBack"]) {
Expand Down Expand Up @@ -185,6 +187,22 @@ - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result {
result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]);
}

- (void)onLoadFile:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* error = nil;
if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) {
result([FlutterError errorWithCode:@"loadFile_failed"
message:@"Failed parsing file path."
details:error]);
return;
}

NSURL* url = [NSURL fileURLWithPath:[call arguments] isDirectory:NO];
Copy link
Contributor

Choose a reason for hiding this comment

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

You should validate that this is non-nil before passing it below (returning an error if it is nil) in case someone passes a garbage string that doesn't parse as a path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added additional check to ensure the url variable is not nil, return an error if the variable is nil.

NSURL* baseUrl = [url URLByDeletingLastPathComponent];

[_webView loadFileURL:url allowingReadAccessToURL:baseUrl];
result(nil);
}

- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
if (![self loadRequest:[call arguments]]) {
result([FlutterError
Expand Down Expand Up @@ -517,6 +535,29 @@ - (void)updateUserAgent:(NSString*)userAgent {
}
}

+ (bool)isValidStringArgument:(id)argument withErrorMessage:(NSString**)errorDetails {
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a declaration comment, per style guide.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oops, and I missed that this is bool; it should be BOOL.

if (!argument) {
if (errorDetails) {
*errorDetails = @"Argument is nil.";
}
return NO;
}
if (![argument isKindOfClass:NSString.class]) {
if (errorDetails) {
*errorDetails = @"Argument is not of type NSString.";
}
return NO;
}
if (![argument length]) {
if (errorDetails) {
*errorDetails = @"Argument contains an empty string.";
}
return NO;
}

return YES;
}

#pragma mark WKUIDelegate

- (WKWebView*)webView:(WKWebView*)webView
Expand Down