-
-
Notifications
You must be signed in to change notification settings - Fork 685
/
test_webview.py
296 lines (240 loc) · 9.23 KB
/
test_webview.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import asyncio
import gc
from asyncio import wait_for
from contextlib import nullcontext
from time import time
from unittest.mock import ANY, Mock
import pytest
import toga
from toga.style import Pack
from .properties import ( # noqa: F401
test_flex_widget_size,
test_focus,
)
# These timeouts are loose because CI can be very slow, especially on mobile.
LOAD_TIMEOUT = 30
JS_TIMEOUT = 5
WINDOWS_INIT_TIMEOUT = 60
async def get_content(widget):
try:
return await wait_for(
widget.evaluate_javascript("document.body.innerHTML"),
JS_TIMEOUT,
)
except asyncio.TimeoutError:
# On Android, if you call evaluate_javascript while a page is loading, the
# callback may never be called. This seems to be associated with the log message
# "Uncaught TypeError: Cannot read property 'innerHTML' of null".
return None
async def assert_content_change(widget, probe, message, url, content, on_load):
# Web views aren't instantaneous. Even for simple static changes of page
# content, the DOM won't be immediately rendered. As a result, even though a
# page loaded signal has been received, it doesn't mean the accessors for
# the page URL or DOM content has been updated in the widget. This is a
# problem for tests, as we need to "make change, test change occurred" with
# as little delay as possible. So - wait for up to 2 seconds for the URL
# *and* content to change in any way before asserting the new values.
changed = False
timer = LOAD_TIMEOUT
await probe.redraw(message)
# Loop until a change occurs
while timer > 0 and not changed:
new_url = widget.url
new_content = await get_content(widget)
changed = new_url == url and new_content == content
if not changed:
timer -= 0.05
await asyncio.sleep(0.05)
if not changed:
pytest.fail(f"{new_url=!r}, {url=!r}, {new_content[:50]=!r}, {content=!r}")
if not probe.supports_on_load:
on_load.assert_not_called()
else:
# Loop until an event occurs
while timer > 0 and not on_load.mock_calls:
timer -= 0.05
await asyncio.sleep(0.05)
on_load.assert_called_with(widget)
@pytest.fixture
async def on_load():
on_load = Mock()
return on_load
@pytest.fixture
async def widget(on_load):
if toga.platform.current_platform == "linux":
# On Gtk, ensure that the WebView from a previous test run is garbage collected.
# This prevents a segfault at GC time likely coming from the test suite running
# in a thread and Gtk WebViews sharing resources between instances. We perform
# the GC run here since pytest fixtures make earlier cleanup difficult.
gc.collect()
widget = toga.WebView(style=Pack(flex=1), on_webview_load=on_load)
# We shouldn't be able to get a callback until at least one tick of the event loop
# has completed.
on_load.assert_not_called()
# On Windows, the WebView has an asynchronous initialization process. Before we
# start the test, make sure initialization is complete by checking the user agent.
deadline = time() + WINDOWS_INIT_TIMEOUT
while True:
try:
# Default user agents are a mess, but they all start with "Mozilla/5.0"
ua = widget.user_agent
assert ua.startswith("Mozilla/5.0 (")
break
except AssertionError:
# On Windows, user_agent will return an empty string during initialization.
if (
toga.platform.current_platform == "windows"
and ua == ""
and time() < deadline
):
await asyncio.sleep(0.05)
else:
raise
yield widget
if toga.platform.current_platform == "linux":
# On Gtk, ensure that the WebView is garbage collection before the next test
# case. This prevents a segfault at GC time likely coming from the test suite
# running in a thread and Gtk WebViews sharing resources between instances.
del widget
gc.collect()
async def test_set_url(widget, probe, on_load):
"The URL can be set"
widget.url = "https://github.com/beeware"
# Wait for the content to be loaded
await assert_content_change(
widget,
probe,
message="Page has been loaded",
url="https://github.com/beeware",
content=ANY,
on_load=on_load,
)
async def test_clear_url(widget, probe, on_load):
"The URL can be cleared"
widget.url = None
# Wait for the content to be cleared
await assert_content_change(
widget,
probe,
message="Page has been cleared",
url=None,
content="",
on_load=on_load,
)
async def test_load_empty_url(widget, probe, on_load):
"An empty URL can be loaded asynchronously into the view"
await wait_for(
widget.load_url(None),
LOAD_TIMEOUT,
)
# DOM loads aren't instantaneous; wait for the URL to appear
await assert_content_change(
widget,
probe,
message="Page has been cleared",
url=None,
content="",
on_load=on_load,
)
async def test_load_url(widget, probe, on_load):
"A URL can be loaded into the view"
await wait_for(
widget.load_url("https://github.com/beeware"),
LOAD_TIMEOUT,
)
# DOM loads aren't instantaneous; wait for the URL to appear
await assert_content_change(
widget,
probe,
message="Page has been loaded",
url="https://github.com/beeware",
content=ANY,
on_load=on_load,
)
async def test_static_content(widget, probe, on_load):
"Static content can be loaded into the page"
widget.set_content("https://example.com/", "<h1>Nice page</h1>")
# DOM loads aren't instantaneous; wait for the URL to appear
await assert_content_change(
widget,
probe,
message="Webview has static content",
url="https://example.com/" if probe.content_supports_url else None,
content="<h1>Nice page</h1>",
on_load=on_load,
)
async def test_user_agent(widget, probe):
"The user agent can be customized"
# The default user agent is tested by the `widget` fixture.
widget.user_agent = "NCSA_Mosaic/1.0"
await probe.redraw("User agent has been customized")
assert widget.user_agent == "NCSA_Mosaic/1.0"
async def test_evaluate_javascript(widget, probe):
"JavaScript can be evaluated"
on_result_handler = Mock()
for expression, expected in [
("37 + 42", 79),
("'awesome'.includes('we')", True),
("'hello js'", "hello js"),
]:
# reset the mock for each pass
on_result_handler.reset_mock()
with pytest.warns(
DeprecationWarning,
match=r"Synchronous `on_result` handlers have been deprecated;",
):
result = await wait_for(
widget.evaluate_javascript(expression, on_result=on_result_handler),
JS_TIMEOUT,
)
# The resulting value has been converted into Python
assert result == expected
# The same value was passed to the on-result handler
on_result_handler.assert_called_once_with(expected)
async def test_evaluate_javascript_no_handler(widget, probe):
"A handler isn't needed to evaluate JavaScript"
result = await wait_for(
widget.evaluate_javascript("37 + 42"),
JS_TIMEOUT,
)
# The resulting value has been converted into Python
assert result == 79
def javascript_error_context(probe):
if probe.javascript_supports_exception:
return pytest.raises(RuntimeError)
else:
return nullcontext()
async def test_evaluate_javascript_error(widget, probe):
"If JavaScript content raises an error, the error is propagated"
on_result_handler = Mock()
with javascript_error_context(probe):
with pytest.warns(
DeprecationWarning,
match=r"Synchronous `on_result` handlers have been deprecated;",
):
result = await wait_for(
widget.evaluate_javascript("not valid js", on_result=on_result_handler),
JS_TIMEOUT,
)
# If the backend supports exceptions, the previous line should have raised one.
assert not probe.javascript_supports_exception
assert result is None
# The same value was passed to the on-result handler
on_result_handler.assert_called_once()
assert on_result_handler.call_args.args == (None,)
kwargs = on_result_handler.call_args.kwargs
if probe.javascript_supports_exception:
assert sorted(kwargs) == ["exception"]
assert isinstance(kwargs["exception"], RuntimeError)
else:
assert kwargs == {}
async def test_evaluate_javascript_error_without_handler(widget, probe):
"A handler isn't needed to propagate a JavaScript error"
with javascript_error_context(probe):
result = await wait_for(
widget.evaluate_javascript("not valid js"),
JS_TIMEOUT,
)
# If the backend supports exceptions, the previous line should have raised one.
assert not probe.javascript_supports_exception
assert result is None