[web] Migrate Flutter Web DOM usage to JS static interop - 22. (flutter/engine#33350)

This commit is contained in:
joshualitt
2022-06-01 09:24:06 -07:00
committed by GitHub
parent 790b9970d8
commit 1dd8b0af78
8 changed files with 119 additions and 70 deletions

View File

@@ -2,11 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html' as html;
import 'package:ui/ui.dart' as ui;
import 'browser_detection.dart';
import 'dom.dart';
import 'services.dart';
import 'util.dart';
@@ -90,7 +89,7 @@ class ClipboardMessageHandler {
/// APIs and the browser.
abstract class CopyToClipboardStrategy {
factory CopyToClipboardStrategy() {
return !unsafeIsNull(html.window.navigator.clipboard)
return !unsafeIsNull(domWindow.navigator.clipboard)
? ClipboardAPICopyStrategy()
: ExecCommandCopyStrategy();
}
@@ -110,7 +109,7 @@ abstract class CopyToClipboardStrategy {
abstract class PasteFromClipboardStrategy {
factory PasteFromClipboardStrategy() {
return (browserEngine == BrowserEngine.firefox ||
unsafeIsNull(html.window.navigator.clipboard))
unsafeIsNull(domWindow.navigator.clipboard))
? ExecCommandPasteStrategy()
: ClipboardAPIPasteStrategy();
}
@@ -127,7 +126,7 @@ class ClipboardAPICopyStrategy implements CopyToClipboardStrategy {
@override
Future<bool> setData(String? text) async {
try {
await html.window.navigator.clipboard!.writeText(text!);
await domWindow.navigator.clipboard!.writeText(text!);
} catch (error) {
print('copy is not successful $error');
return Future<bool>.value(false);
@@ -145,7 +144,7 @@ class ClipboardAPICopyStrategy implements CopyToClipboardStrategy {
class ClipboardAPIPasteStrategy implements PasteFromClipboardStrategy {
@override
Future<String> getData() async {
return html.window.navigator.clipboard!.readText();
return domWindow.navigator.clipboard!.readText();
}
}
@@ -159,13 +158,13 @@ class ExecCommandCopyStrategy implements CopyToClipboardStrategy {
bool _setDataSync(String? text) {
// Copy content to clipboard with execCommand.
// See: https://developers.google.com/web/updates/2015/04/cut-and-copy-commands
final html.TextAreaElement tempTextArea = _appendTemporaryTextArea();
final DomHTMLTextAreaElement tempTextArea = _appendTemporaryTextArea();
tempTextArea.value = text;
tempTextArea.focus();
tempTextArea.select();
bool result = false;
try {
result = html.document.execCommand('copy');
result = domDocument.execCommand('copy');
if (!result) {
print('copy is not successful');
}
@@ -177,9 +176,9 @@ class ExecCommandCopyStrategy implements CopyToClipboardStrategy {
return result;
}
html.TextAreaElement _appendTemporaryTextArea() {
final html.TextAreaElement tempElement = html.TextAreaElement();
final html.CssStyleDeclaration elementStyle = tempElement.style;
DomHTMLTextAreaElement _appendTemporaryTextArea() {
final DomHTMLTextAreaElement tempElement = createDomHTMLTextAreaElement();
final DomCSSStyleDeclaration elementStyle = tempElement.style;
elementStyle
..position = 'absolute'
..top = '-99999px'
@@ -189,12 +188,12 @@ class ExecCommandCopyStrategy implements CopyToClipboardStrategy {
..backgroundColor = 'transparent'
..background = 'transparent';
html.document.body!.append(tempElement);
domDocument.body!.append(tempElement);
return tempElement;
}
void _removeTemporaryTextArea(html.HtmlElement element) {
void _removeTemporaryTextArea(DomHTMLElement element) {
element.remove();
}
}

View File

@@ -29,6 +29,7 @@ extension DomWindowExtension on DomWindow {
external int? get innerHeight;
external int? get innerWidth;
external DomNavigator get navigator;
external DomVisualViewport? get visualViewport;
external DomPerformance get performance;
Future<Object?> fetch(String url) =>
js_util.promiseToFuture(js_util.callMethod(this, 'fetch', <String>[url]));
@@ -50,6 +51,7 @@ external DomWindow get domWindow;
class DomNavigator {}
extension DomNavigatorExtension on DomNavigator {
external DomClipboard? get clipboard;
external int? get maxTouchPoints;
external String get vendor;
external String get language;
@@ -62,6 +64,7 @@ extension DomNavigatorExtension on DomNavigator {
class DomDocument {}
extension DomDocumentExtension on DomDocument {
external DomElement? get documentElement;
external DomElement? querySelector(String selectors);
List<DomElement> querySelectorAll(String selectors) =>
js_util.callMethod<List<Object?>>(
@@ -69,6 +72,7 @@ extension DomDocumentExtension on DomDocument {
DomElement createElement(String name, [Object? options]) =>
js_util.callMethod(this, 'createElement',
<Object>[name, if (options != null) options]) as DomElement;
external bool execCommand(String commandId);
external DomHTMLScriptElement? get currentScript;
external DomElement createElementNS(
String namespaceURI, String qualifiedName);
@@ -167,6 +171,8 @@ DomElement createDomElement(String tag) => domDocument.createElement(tag);
extension DomElementExtension on DomElement {
List<DomElement> get children =>
js_util.getProperty<List<Object?>>(this, 'children').cast<DomElement>();
external int get clientHeight;
external int get clientWidth;
external String get id;
external set id(String id);
external set innerHtml(String? html);
@@ -254,6 +260,7 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration {
set flexDirection(String value) => setProperty('flex-direction', value);
set alignItems(String value) => setProperty('align-items', value);
set margin(String value) => setProperty('margin', value);
set background(String value) => setProperty('background', value);
String get width => getPropertyValue('width');
String get height => getPropertyValue('height');
String get position => getPropertyValue('position');
@@ -308,6 +315,7 @@ extension DomCSSStyleDeclarationExtension on DomCSSStyleDeclaration {
String get flexDirection => getPropertyValue('flex-direction');
String get alignItems => getPropertyValue('align-items');
String get margin => getPropertyValue('margin');
String get background=> getPropertyValue('background');
external String getPropertyValue(String property);
void setProperty(String propertyName, String value, [String? priority]) {
@@ -325,6 +333,7 @@ class DomHTMLElement extends DomElement {}
extension DomHTMLElementExtension on DomHTMLElement {
int get offsetWidth => js_util.getProperty<num>(this, 'offsetWidth') as int;
external void focus();
}
@JS()
@@ -376,6 +385,13 @@ class DomHTMLDivElement extends DomHTMLElement {}
DomHTMLDivElement createDomHTMLDivElement() =>
domDocument.createElement('div') as DomHTMLDivElement;
@JS()
@staticInterop
class DomHTMLSpanElement extends DomHTMLElement {}
DomHTMLSpanElement createDomHTMLSpanElement() =>
domDocument.createElement('span') as DomHTMLSpanElement;
@JS()
@staticInterop
class DomHTMLButtonElement extends DomHTMLElement {}
@@ -610,6 +626,39 @@ extension DomFontFaceSetExtension on DomFontFaceSet {
external void clear();
}
@JS()
@staticInterop
class DomVisualViewport extends DomEventTarget {}
extension DomVisualViewportExtension on DomVisualViewport {
external num? get height;
external num? get width;
}
@JS()
@staticInterop
class DomHTMLTextAreaElement extends DomHTMLElement {}
DomHTMLTextAreaElement createDomHTMLTextAreaElement() =>
domDocument.createElement('textarea') as DomHTMLTextAreaElement;
extension DomHTMLTextAreaElementExtension on DomHTMLTextAreaElement {
external set value(String? value);
external void select();
}
@JS()
@staticInterop
class DomClipboard extends DomEventTarget {}
extension DomClipboardExtension on DomClipboard {
Future<String> readText() =>
js_util.promiseToFuture<String>(js_util.callMethod(this, 'readText', <Object>[]));
Future<dynamic> writeText(String data) =>
js_util.promiseToFuture(js_util.callMethod(this, 'readText', <Object>[data]));
}
extension DomResponseExtension on DomResponse {
Future<dynamic> arrayBuffer() => js_util
.promiseToFuture(js_util.callMethod(this, 'arrayBuffer', <Object>[]));

View File

@@ -15,6 +15,7 @@ import 'canvaskit/initialization.dart';
import 'canvaskit/layer_scene_builder.dart';
import 'canvaskit/rasterizer.dart';
import 'clipboard.dart';
import 'dom.dart';
import 'embedder.dart';
import 'html/scene.dart';
import 'mouse_cursor.dart';
@@ -482,8 +483,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
case 'flutter/platform_views':
_platformViewMessageHandler ??= PlatformViewMessageHandler(
contentManager: platformViewManager,
contentHandler: (html.Element content) {
flutterViewEmbedder.glassPaneElement!.append(content);
contentHandler: (DomElement content) {
// Remove cast to [html.Element] after migration.
flutterViewEmbedder.glassPaneElement!.append(content as html.Element);
},
);
_platformViewMessageHandler!.handlePlatformViewCall(data, callback!);

View File

@@ -5,6 +5,7 @@
import 'dart:html' as html;
import '../browser_detection.dart';
import '../dom.dart';
import '../embedder.dart';
import '../util.dart';
import 'slots.dart';
@@ -12,7 +13,7 @@ import 'slots.dart';
/// A function which takes a unique `id` and some `params` and creates an HTML element.
///
/// This is made available to end-users through dart:ui in web.
typedef ParameterizedPlatformViewFactory = html.Element Function(
typedef ParameterizedPlatformViewFactory = DomElement Function(
int viewId, {
Object? params,
});
@@ -20,7 +21,7 @@ typedef ParameterizedPlatformViewFactory = html.Element Function(
/// A function which takes a unique `id` and creates an HTML element.
///
/// This is made available to end-users through dart:ui in web.
typedef PlatformViewFactory = html.Element Function(int viewId);
typedef PlatformViewFactory = DomElement Function(int viewId);
/// This class handles the lifecycle of Platform Views in the DOM of a Flutter Web App.
///
@@ -29,7 +30,7 @@ typedef PlatformViewFactory = html.Element Function(int viewId);
///
/// * `factories`: The functions used to render the contents of any given Platform
/// View by its `viewType`.
/// * `contents`: The result [html.Element] of calling a `factory` function.
/// * `contents`: The result [DomElement] of calling a `factory` function.
///
/// The third part is `slots`, which are created on demand by the
/// [createPlatformViewSlot] function.
@@ -41,7 +42,7 @@ class PlatformViewManager {
final Map<String, Function> _factories = <String, Function>{};
// The references to content tags, indexed by their framework-given ID.
final Map<int, html.Element> _contents = <int, html.Element>{};
final Map<int, DomElement> _contents = <int, DomElement>{};
final Set<String> _invisibleViews = <String>{};
final Map<int, String> _viewIdToType = <int, String>{};
@@ -103,7 +104,7 @@ class PlatformViewManager {
/// a place where to attach the `slot` property, that will tell the browser
/// what `slot` tag will reveal this `contents`, **without modifying the returned
/// html from the `factory` function**.
html.Element renderContent(
DomElement renderContent(
String viewType,
int viewId,
Object? params,
@@ -115,12 +116,12 @@ class PlatformViewManager {
_viewIdToType[viewId] = viewType;
return _contents.putIfAbsent(viewId, () {
final html.Element wrapper = html.document
final DomElement wrapper = domDocument
.createElement('flt-platform-view')
..setAttribute('slot', slotName);
final Function factoryFunction = _factories[viewType]!;
late html.Element content;
late DomElement content;
if (factoryFunction is ParameterizedPlatformViewFactory) {
content = factoryFunction(viewId, params: params);
@@ -140,7 +141,7 @@ class PlatformViewManager {
/// never been rendered before.
void clearPlatformView(int viewId) {
// Remove from our cache, and then from the DOM...
final html.Element? element = _contents.remove(viewId);
final DomElement? element = _contents.remove(viewId);
_safelyRemoveSlottedElement(element);
}
@@ -149,7 +150,7 @@ class PlatformViewManager {
// than its slot (after the slot is removed).
//
// TODO(web): Cleanup https://github.com/flutter/flutter/issues/85816
void _safelyRemoveSlottedElement(html.Element? element) {
void _safelyRemoveSlottedElement(DomElement? element) {
if (element == null) {
return;
}
@@ -159,10 +160,11 @@ class PlatformViewManager {
}
final String tombstoneName = "tombstone-${element.getAttribute('slot')}";
// Create and inject a new slot in the shadow root
final html.Element slot = html.document.createElement('slot')
final DomElement slot = domDocument.createElement('slot')
..style.display = 'none'
..setAttribute('name', tombstoneName);
flutterViewEmbedder.glassPaneShadow!.append(slot);
// Remove cast to [html.Node] after migration.
flutterViewEmbedder.glassPaneShadow!.append(slot as html.Node);
// Link the element to the new slot
element.setAttribute('slot', tombstoneName);
// Delete both the element, and the new slot
@@ -172,7 +174,7 @@ class PlatformViewManager {
/// Attempt to ensure that the contents of the user-supplied DOM element will
/// fill the space allocated for this platform view by the framework.
void _ensureContentCorrectlySized(html.Element content, String viewType) {
void _ensureContentCorrectlySized(DomElement content, String viewType) {
// Scrutinize closely any other modifications to `content`.
// We shouldn't modify users' returned `content` if at all possible.
// Note there's also no getContent(viewId) function anymore, to prevent

View File

@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html' as html;
import 'dart:typed_data';
import '../dom.dart';
import '../services.dart';
import '../util.dart';
import 'content_manager.dart';
@@ -13,9 +13,9 @@ import 'content_manager.dart';
/// Copied here so there's no circular dependencies.
typedef _PlatformMessageResponseCallback = void Function(ByteData? data);
/// A function that handle a newly created [html.Element] with the contents of a
/// A function that handle a newly created [DomElement] with the contents of a
/// platform view with a unique [int] id.
typedef PlatformViewContentHandler = void Function(html.Element);
typedef PlatformViewContentHandler = void Function(DomElement);
/// This class handles incoming framework messages to create/dispose Platform Views.
///
@@ -91,7 +91,7 @@ class PlatformViewMessageHandler {
}
// TODO(hterkelsen): How can users add extra `args` from the HtmlElementView widget?
final html.Element content = _contentManager.renderContent(
final DomElement content = _contentManager.renderContent(
viewType,
viewId,
args,

View File

@@ -6,7 +6,6 @@
library window;
import 'dart:async';
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:js/js.dart';
@@ -15,6 +14,7 @@ import 'package:ui/ui.dart' as ui;
import '../engine.dart' show registerHotRestartListener;
import 'browser_detection.dart';
import 'dom.dart';
import 'navigation/history.dart';
import 'navigation/js_url_strategy.dart';
import 'navigation/url_strategy.dart';
@@ -215,7 +215,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
return _physicalSize!;
}
/// Computes the physical size of the screen from [html.window].
/// Computes the physical size of the screen from [domWindow].
///
/// This function is expensive. It triggers browser layout if there are
/// pending DOM writes.
@@ -233,7 +233,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
if (!override) {
double windowInnerWidth;
double windowInnerHeight;
final html.VisualViewport? viewport = html.window.visualViewport;
final DomVisualViewport? viewport = domWindow.visualViewport;
if (viewport != null) {
if (operatingSystem == OperatingSystem.iOs) {
@@ -246,9 +246,9 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
/// text editing to make sure inset is correctly reported to
/// framework.
final double docWidth =
html.document.documentElement!.clientWidth.toDouble();
domDocument.documentElement!.clientWidth.toDouble();
final double docHeight =
html.document.documentElement!.clientHeight.toDouble();
domDocument.documentElement!.clientHeight.toDouble();
windowInnerWidth = docWidth * devicePixelRatio;
windowInnerHeight = docHeight * devicePixelRatio;
} else {
@@ -256,8 +256,8 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
windowInnerHeight = viewport.height!.toDouble() * devicePixelRatio;
}
} else {
windowInnerWidth = html.window.innerWidth! * devicePixelRatio;
windowInnerHeight = html.window.innerHeight! * devicePixelRatio;
windowInnerWidth = domWindow.innerWidth! * devicePixelRatio;
windowInnerHeight = domWindow.innerHeight! * devicePixelRatio;
}
_physicalSize = ui.Size(
windowInnerWidth,
@@ -273,16 +273,16 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
void computeOnScreenKeyboardInsets(bool isEditingOnMobile) {
double windowInnerHeight;
final html.VisualViewport? viewport = html.window.visualViewport;
final DomVisualViewport? viewport = domWindow.visualViewport;
if (viewport != null) {
if (operatingSystem == OperatingSystem.iOs && !isEditingOnMobile) {
windowInnerHeight =
html.document.documentElement!.clientHeight * devicePixelRatio;
domDocument.documentElement!.clientHeight * devicePixelRatio;
} else {
windowInnerHeight = viewport.height!.toDouble() * devicePixelRatio;
}
} else {
windowInnerHeight = html.window.innerHeight! * devicePixelRatio;
windowInnerHeight = domWindow.innerHeight! * devicePixelRatio;
}
final double bottomPadding = _physicalSize!.height - windowInnerHeight;
_viewInsets =
@@ -306,13 +306,13 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
bool isRotation() {
double height = 0;
double width = 0;
if (html.window.visualViewport != null) {
if (domWindow.visualViewport != null) {
height =
html.window.visualViewport!.height!.toDouble() * devicePixelRatio;
width = html.window.visualViewport!.width!.toDouble() * devicePixelRatio;
domWindow.visualViewport!.height!.toDouble() * devicePixelRatio;
width = domWindow.visualViewport!.width!.toDouble() * devicePixelRatio;
} else {
height = html.window.innerHeight! * devicePixelRatio;
width = html.window.innerWidth! * devicePixelRatio;
height = domWindow.innerHeight! * devicePixelRatio;
width = domWindow.innerWidth! * devicePixelRatio;
}
// This method compares the new dimensions with the previous ones.

View File

@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html' as html;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
@@ -33,7 +31,7 @@ void testMain() {
test('recognizes viewTypes after registering them', () async {
expect(contentManager.knowsViewType(viewType), isFalse);
contentManager.registerFactory(viewType, (int id) => html.DivElement());
contentManager.registerFactory(viewType, (int id) => createDomHTMLDivElement());
expect(contentManager.knowsViewType(viewType), isTrue);
});
@@ -43,7 +41,7 @@ void testMain() {
test('recognizes viewIds after *rendering* them', () async {
expect(contentManager.knowsViewId(viewId), isFalse);
contentManager.registerFactory(viewType, (int id) => html.DivElement());
contentManager.registerFactory(viewType, (int id) => createDomHTMLDivElement());
expect(contentManager.knowsViewId(viewId), isFalse);
@@ -53,7 +51,7 @@ void testMain() {
});
test('forgets viewIds after clearing them', () {
contentManager.registerFactory(viewType, (int id) => html.DivElement());
contentManager.registerFactory(viewType, (int id) => createDomHTMLDivElement());
contentManager.renderContent(viewType, viewId, null);
expect(contentManager.knowsViewId(viewId), isTrue);
@@ -67,12 +65,12 @@ void testMain() {
group('registerFactory', () {
test('does NOT re-register factories', () async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement()..id = 'pass');
viewType, (int id) => createDomHTMLDivElement()..id = 'pass');
// this should be rejected
contentManager.registerFactory(
viewType, (int id) => html.SpanElement()..id = 'fail');
viewType, (int id) => createDomHTMLSpanElement()..id = 'fail');
final html.Element contents =
final DomElement contents =
contentManager.renderContent(viewType, viewId, null);
expect(contents.querySelector('#pass'), isNotNull);
@@ -87,11 +85,11 @@ void testMain() {
setUp(() {
contentManager.registerFactory(viewType, (int id) {
return html.DivElement()..setAttribute('data-viewId', '$id');
return createDomHTMLDivElement()..setAttribute('data-viewId', '$id');
});
contentManager.registerFactory(anotherViewType, (int id) {
return html.DivElement()
return createDomHTMLDivElement()
..setAttribute('data-viewId', '$id')
..style.height = 'auto'
..style.width = '55%';
@@ -109,17 +107,17 @@ void testMain() {
});
test('rendered markup contains required attributes', () async {
final html.Element content =
final DomElement content =
contentManager.renderContent(viewType, viewId, null);
expect(content.getAttribute('slot'), contains('$viewId'));
final html.Element userContent = content.querySelector('div')!;
final DomElement userContent = content.querySelector('div')!;
expect(userContent.style.height, '100%');
expect(userContent.style.width, '100%');
});
test('slot property has the same value as createPlatformViewSlot', () async {
final html.Element content =
final DomElement content =
contentManager.renderContent(viewType, viewId, null);
final DomElement slot = createPlatformViewSlot(viewId);
final DomElement innerSlot = slot.querySelector('slot')!;
@@ -131,17 +129,17 @@ void testMain() {
test('do not modify style.height / style.width if passed by the user (anotherViewType)',
() async {
final html.Element content =
final DomElement content =
contentManager.renderContent(anotherViewType, viewId, null);
final html.Element userContent = content.querySelector('div')!;
final DomElement userContent = content.querySelector('div')!;
expect(userContent.style.height, 'auto');
expect(userContent.style.width, '55%');
});
test('returns cached instances of already-rendered content', () async {
final html.Element firstRender =
final DomElement firstRender =
contentManager.renderContent(viewType, viewId, null);
final html.Element anotherRender =
final DomElement anotherRender =
contentManager.renderContent(viewType, viewId, null);
expect(firstRender, same(anotherRender));

View File

@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
@@ -23,12 +22,12 @@ void testMain() {
const int viewId = 6;
late PlatformViewManager contentManager;
late Completer<ByteData?> completer;
late Completer<html.Element> contentCompleter;
late Completer<DomElement> contentCompleter;
setUp(() {
contentManager = PlatformViewManager();
completer = Completer<ByteData?>();
contentCompleter = Completer<html.Element>();
contentCompleter = Completer<DomElement>();
});
group('"create" message', () {
@@ -53,7 +52,7 @@ void testMain() {
test('duplicate viewId, fails with descriptive exception', () async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement());
viewType, (int id) => createDomHTMLDivElement());
contentManager.renderContent(viewType, viewId, null);
final PlatformViewMessageHandler messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
@@ -74,7 +73,7 @@ void testMain() {
test('returns a successEnvelope when the view is created normally',
() async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement()..id = 'success');
viewType, (int id) => createDomHTMLDivElement()..id = 'success');
final PlatformViewMessageHandler messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
);
@@ -91,7 +90,7 @@ void testMain() {
test('calls a contentHandler with the result of creating a view',
() async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement()..id = 'success');
viewType, (int id) => createDomHTMLDivElement()..id = 'success');
final PlatformViewMessageHandler messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
contentHandler: contentCompleter.complete,
@@ -100,7 +99,7 @@ void testMain() {
messageHandler.handlePlatformViewCall(message, completer.complete);
final html.Element contents = await contentCompleter.future;
final DomElement contents = await contentCompleter.future;
final ByteData? response = await completer.future;
expect(contents.querySelector('div#success'), isNotNull,