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 all 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
11 changes: 9 additions & 2 deletions packages/webview_flutter/webview_flutter_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## NEXT

## 0.2.2

* Updates `WebWebViewController.loadRequest` to only set the src of the iFrame
when `LoadRequestParams.headers` and `LoadRequestParams.body` are empty and is
using the HTTP GET request method. [#118573](https://github.com/flutter/flutter/issues/118573).
* Parses the `content-type` header of XHR responses to extract the correct
MIME-type and charset. [#118090](https://github.com/flutter/flutter/issues/118090).
* Sets `width` and `height` of widget the way the Engine wants, to remove distracting
warnings from the development console.
* Updates minimum Flutter version to 3.0.

## 0.2.1
Expand Down
16 changes: 16 additions & 0 deletions packages/webview_flutter/webview_flutter_web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,19 @@ yet, so it currently requires extra setup to use:

Once the step above is complete, the APIs from `webview_flutter` listed
above can be used as normal on web.

## Tests

Tests are contained in the `test` directory. You can run all tests from the root
of the package with the following command:

```bash
$ flutter test --platform chrome
```

This package uses `package:mockito` in some tests. Mock files can be updated
from the root of the package like so:

```bash
$ flutter pub run build_runner build --delete-conflicting-outputs
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import 'dart:html' as html;
import 'dart:io';

// FIX (dit): Remove these integration tests, or make them run. They currently never fail.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I didn't recognize this when I updated these tests. Thanks for noticing this!

// (They won't run because they use `dart:io`. If you remove all `dart:io` bits from
// this file, they start failing with `fail()`, for example.)

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/bash
# 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.

if pgrep -lf chromedriver > /dev/null; then
echo "chromedriver is running."

if [ $# -eq 0 ]; then
echo "No target specified, running all tests..."
find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}'
else
echo "Running test target: $1..."
set -x
flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1
fi

else
echo "chromedriver is not running."
echo "Please, check the README.md for instructions on how to use run_test.sh"
fi

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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.

/// Class to represent a content-type header value.
class ContentType {
/// Creates a [ContentType] instance by parsing a "content-type" response [header].
///
/// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
/// See: https://httpwg.org/specs/rfc9110.html#media.type
ContentType.parse(String header) {
final Iterable<String> chunks =
header.split(';').map((String e) => e.trim().toLowerCase());

for (final String chunk in chunks) {
if (!chunk.contains('=')) {
_mimeType = chunk;
} else {
final List<String> bits =
chunk.split('=').map((String e) => e.trim()).toList();
assert(bits.length == 2);
switch (bits[0]) {
case 'charset':
_charset = bits[1];
break;
case 'boundary':
_boundary = bits[1];
break;
default:
throw StateError('Unable to parse "$chunk" in content-type.');
}
}
}
}

String? _mimeType;
String? _charset;
String? _boundary;

/// The MIME-type of the resource or the data.
String? get mimeType => _mimeType;

/// The character encoding standard.
String? get charset => _charset;

/// The separation boundary for multipart entities.
String? get boundary => _boundary;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
// found in the LICENSE file.

import 'dart:convert';
import 'dart:html';
import 'dart:html' as html;

import 'package:flutter/cupertino.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

import 'content_type.dart';
import 'http_request_factory.dart';
import 'shims/dart_ui.dart' as ui;

Expand Down Expand Up @@ -37,10 +38,10 @@ class WebWebViewControllerCreationParams

/// The underlying element used as the WebView.
@visibleForTesting
final IFrameElement iFrame = IFrameElement()
final html.IFrameElement iFrame = html.IFrameElement()
..id = 'webView${_nextIFrameId++}'
..width = '100%'
..height = '100%'
..style.width = '100%'
..style.height = '100%'
..style.border = 'none';
}

Expand Down Expand Up @@ -72,20 +73,37 @@ class WebWebViewController extends PlatformWebViewController {
throw ArgumentError(
'LoadRequestParams#uri is required to have a scheme.');
}
final HttpRequest httpReq =

if (params.headers.isEmpty &&
(params.body == null || params.body!.isEmpty) &&
params.method == LoadRequestMethod.get) {
// ignore: unsafe_html
_webWebViewParams.iFrame.src = params.uri.toString();
} else {
await _updateIFrameFromXhr(params);
}
}

/// Performs an AJAX request defined by [params].
Future<void> _updateIFrameFromXhr(LoadRequestParams params) async {
final html.HttpRequest httpReq =
await _webWebViewParams.httpRequestFactory.request(
params.uri.toString(),
method: params.method.serialize(),
requestHeaders: params.headers,
sendData: params.body,
);
final String contentType =

final String header =
httpReq.getResponseHeader('content-type') ?? 'text/html';
final ContentType contentType = ContentType.parse(header);
final Encoding encoding = Encoding.getByName(contentType.charset) ?? utf8;

// ignore: unsafe_html
_webWebViewParams.iFrame.src = Uri.dataFromString(
httpReq.responseText ?? '',
mimeType: contentType,
encoding: utf8,
mimeType: contentType.mimeType,
encoding: encoding,
).toString();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter_web
description: A Flutter plugin that provides a WebView widget on web.
repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 0.2.1
version: 0.2.2

environment:
sdk: ">=2.14.0 <3.0.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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:flutter_test/flutter_test.dart';
import 'package:webview_flutter_web/src/content_type.dart';

void main() {
group('ContentType.parse', () {
test('basic content-type (lowers case)', () {
final ContentType contentType = ContentType.parse('text/pLaIn');

expect(contentType.mimeType, 'text/plain');
expect(contentType.boundary, isNull);
expect(contentType.charset, isNull);
});

test('with charset', () {
final ContentType contentType =
ContentType.parse('text/pLaIn; charset=utf-8');

expect(contentType.mimeType, 'text/plain');
expect(contentType.boundary, isNull);
expect(contentType.charset, 'utf-8');
});

test('with boundary', () {
final ContentType contentType =
ContentType.parse('text/pLaIn; boundary=---xyz');

expect(contentType.mimeType, 'text/plain');
expect(contentType.boundary, '---xyz');
expect(contentType.charset, isNull);
});

test('with charset and boundary', () {
final ContentType contentType =
ContentType.parse('text/pLaIn; charset=utf-8; boundary=---xyz');

expect(contentType.mimeType, 'text/plain');
expect(contentType.boundary, '---xyz');
expect(contentType.charset, 'utf-8');
});

test('with boundary and charset', () {
final ContentType contentType =
ContentType.parse('text/pLaIn; boundary=---xyz; charset=utf-8');

expect(contentType.mimeType, 'text/plain');
expect(contentType.boundary, '---xyz');
expect(contentType.charset, 'utf-8');
});

test('with a bunch of whitespace, boundary and charset', () {
final ContentType contentType = ContentType.parse(
' text/pLaIn ; boundary=---xyz; charset=utf-8 ');

expect(contentType.mimeType, 'text/plain');
expect(contentType.boundary, '---xyz');
expect(contentType.charset, 'utf-8');
});

test('empty string', () {
final ContentType contentType = ContentType.parse('');

expect(contentType.mimeType, '');
expect(contentType.boundary, isNull);
expect(contentType.charset, isNull);
});

test('unknown parameter (throws)', () {
expect(() {
ContentType.parse('text/pLaIn; wrong=utf-8');
}, throwsStateError);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:html';
// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231)
// ignore: unnecessary_import
Expand All @@ -17,9 +18,9 @@ import 'package:webview_flutter_web/webview_flutter_web.dart';

import 'web_webview_controller_test.mocks.dart';

@GenerateMocks(<Type>[
HttpRequest,
HttpRequestFactory,
@GenerateMocks(<Type>[], customMocks: <MockSpec<Object>>[
MockSpec<HttpRequest>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<HttpRequestFactory>(onMissingStub: OnMissingStub.returnDefault),
])
void main() {
WidgetsFlutterBinding.ensureInitialized();
Expand All @@ -31,8 +32,8 @@ void main() {
WebWebViewControllerCreationParams();

expect(params.iFrame.id, contains('webView'));
expect(params.iFrame.width, '100%');
expect(params.iFrame.height, '100%');
expect(params.iFrame.style.width, '100%');
expect(params.iFrame.style.height, '100%');
expect(params.iFrame.style.border, 'none');
});
});
Expand Down Expand Up @@ -62,7 +63,7 @@ void main() {
});

group('loadRequest', () {
test('loadRequest throws ArgumentError on missing scheme', () async {
test('throws ArgumentError on missing scheme', () async {
final WebWebViewController controller =
WebWebViewController(WebWebViewControllerCreationParams());

Expand All @@ -73,8 +74,33 @@ void main() {
throwsA(const TypeMatcher<ArgumentError>()));
});

test('loadRequest makes request and loads response into iframe',
() async {
test('skips XHR for simple GETs (no headers, no data)', () async {
final MockHttpRequestFactory mockHttpRequestFactory =
MockHttpRequestFactory();
final WebWebViewController controller =
WebWebViewController(WebWebViewControllerCreationParams(
httpRequestFactory: mockHttpRequestFactory,
));

when(mockHttpRequestFactory.request(
any,
method: anyNamed('method'),
requestHeaders: anyNamed('requestHeaders'),
sendData: anyNamed('sendData'),
)).thenThrow(
StateError('The `request` method should not have been called.'));

await controller.loadRequest(LoadRequestParams(
uri: Uri.parse('https://flutter.dev'),
));

expect(
(controller.params as WebWebViewControllerCreationParams).iFrame.src,
'https://flutter.dev/',
);
});

test('makes request and loads response into iframe', () async {
final MockHttpRequestFactory mockHttpRequestFactory =
MockHttpRequestFactory();
final WebWebViewController controller =
Expand Down Expand Up @@ -114,7 +140,41 @@ void main() {
);
});

test('loadRequest escapes "#" correctly', () async {
test('parses content-type response header correctly', () async {
final MockHttpRequestFactory mockHttpRequestFactory =
MockHttpRequestFactory();
final WebWebViewController controller =
WebWebViewController(WebWebViewControllerCreationParams(
httpRequestFactory: mockHttpRequestFactory,
));

final Encoding iso = Encoding.getByName('latin1')!;

final MockHttpRequest mockHttpRequest = MockHttpRequest();
when(mockHttpRequest.responseText)
.thenReturn(String.fromCharCodes(iso.encode('España')));
when(mockHttpRequest.getResponseHeader('content-type'))
.thenReturn('Text/HTmL; charset=latin1');

when(mockHttpRequestFactory.request(
any,
method: anyNamed('method'),
requestHeaders: anyNamed('requestHeaders'),
sendData: anyNamed('sendData'),
)).thenAnswer((_) => Future<HttpRequest>.value(mockHttpRequest));

await controller.loadRequest(LoadRequestParams(
uri: Uri.parse('https://flutter.dev'),
method: LoadRequestMethod.post,
));

expect(
(controller.params as WebWebViewControllerCreationParams).iFrame.src,
'data:text/html;charset=iso-8859-1,Espa%F1a',
);
});

test('escapes "#" correctly', () async {
final MockHttpRequestFactory mockHttpRequestFactory =
MockHttpRequestFactory();
final WebWebViewController controller =
Expand Down
Loading