@pyscript/core 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/dist/core.js +3 -3
- package/dist/core.js.map +1 -1
- package/docs/README.md +3 -12
- package/package.json +3 -3
- package/src/config.js +32 -24
- package/src/core.js +1 -1
- package/src/stdlib/pyscript/__init__.py +34 -0
- package/src/stdlib/pyscript/display.py +154 -0
- package/src/stdlib/pyscript/event_handling.py +45 -0
- package/src/stdlib/pyscript/magic_js.py +32 -0
- package/src/stdlib/pyscript/util.py +22 -0
- package/src/stdlib/pyscript.js +9 -5
- package/src/stdlib/pyweb/pydom.py +314 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +184 -0
- package/tests/integration/support.py +1038 -0
- package/tests/integration/test_00_support.py +495 -0
- package/tests/integration/test_01_basic.py +353 -0
- package/tests/integration/test_02_display.py +452 -0
- package/tests/integration/test_03_element.py +303 -0
- package/tests/integration/test_assets/line_plot.png +0 -0
- package/tests/integration/test_assets/tripcolor.png +0 -0
- package/tests/integration/test_async.py +197 -0
- package/tests/integration/test_event_handling.py +193 -0
- package/tests/integration/test_importmap.py +66 -0
- package/tests/integration/test_interpreter.py +98 -0
- package/tests/integration/test_plugins.py +419 -0
- package/tests/integration/test_py_config.py +294 -0
- package/tests/integration/test_py_repl.py +663 -0
- package/tests/integration/test_py_terminal.py +270 -0
- package/tests/integration/test_runtime_attributes.py +64 -0
- package/tests/integration/test_script_type.py +121 -0
- package/tests/integration/test_shadow_root.py +33 -0
- package/tests/integration/test_splashscreen.py +124 -0
- package/tests/integration/test_stdio_handling.py +370 -0
- package/tests/integration/test_style.py +47 -0
- package/tests/integration/test_warnings_and_banners.py +32 -0
- package/tests/integration/test_zz_examples.py +419 -0
- package/tests/integration/test_zzz_docs_snippets.py +305 -0
- package/types/stdlib/pyscript.d.ts +8 -4
@@ -0,0 +1,495 @@
|
|
1
|
+
import re
|
2
|
+
import textwrap
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from .support import (
|
7
|
+
PageErrors,
|
8
|
+
PageErrorsDidNotRaise,
|
9
|
+
PyScriptTest,
|
10
|
+
with_execution_thread,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
@with_execution_thread(None)
|
15
|
+
class TestSupport(PyScriptTest):
|
16
|
+
"""
|
17
|
+
These are NOT tests about PyScript.
|
18
|
+
|
19
|
+
They test the PyScriptTest class, i.e. we want to ensure that all the
|
20
|
+
testing machinery that we have works correctly.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def test_basic(self):
|
24
|
+
"""
|
25
|
+
Very basic test, just to check that we can write, serve and read a simple
|
26
|
+
HTML (no pyscript yet)
|
27
|
+
"""
|
28
|
+
doc = """
|
29
|
+
<html>
|
30
|
+
<body>
|
31
|
+
<h1>Hello world</h1>
|
32
|
+
</body>
|
33
|
+
</html>
|
34
|
+
"""
|
35
|
+
self.writefile("mytest.html", doc)
|
36
|
+
self.goto("mytest.html")
|
37
|
+
content = self.page.content()
|
38
|
+
assert "<h1>Hello world</h1>" in content
|
39
|
+
|
40
|
+
def test_await_with_run_js(self):
|
41
|
+
self.run_js(
|
42
|
+
"""
|
43
|
+
function resolveAfter200MilliSeconds(x) {
|
44
|
+
return new Promise((resolve) => {
|
45
|
+
setTimeout(() => {
|
46
|
+
resolve(x);
|
47
|
+
}, 200);
|
48
|
+
});
|
49
|
+
}
|
50
|
+
|
51
|
+
const x = await resolveAfter200MilliSeconds(10);
|
52
|
+
console.log(x);
|
53
|
+
"""
|
54
|
+
)
|
55
|
+
|
56
|
+
assert self.console.log.lines[-1] == "10"
|
57
|
+
|
58
|
+
def test_console(self):
|
59
|
+
"""
|
60
|
+
Test that we capture console.log messages correctly.
|
61
|
+
"""
|
62
|
+
doc = """
|
63
|
+
<html>
|
64
|
+
<body>
|
65
|
+
<script>
|
66
|
+
console.log("my log 1");
|
67
|
+
console.debug("my debug");
|
68
|
+
console.info("my info");
|
69
|
+
console.error("my error");
|
70
|
+
console.warn("my warning");
|
71
|
+
console.log("my log 2");
|
72
|
+
</script>
|
73
|
+
</body>
|
74
|
+
</html>
|
75
|
+
"""
|
76
|
+
self.writefile("mytest.html", doc)
|
77
|
+
self.goto("mytest.html")
|
78
|
+
assert len(self.console.all.messages) == 6
|
79
|
+
assert self.console.all.lines == [
|
80
|
+
"my log 1",
|
81
|
+
"my debug",
|
82
|
+
"my info",
|
83
|
+
"my error",
|
84
|
+
"my warning",
|
85
|
+
"my log 2",
|
86
|
+
]
|
87
|
+
|
88
|
+
# fmt: off
|
89
|
+
assert self.console.all.text == textwrap.dedent("""
|
90
|
+
my log 1
|
91
|
+
my debug
|
92
|
+
my info
|
93
|
+
my error
|
94
|
+
my warning
|
95
|
+
my log 2
|
96
|
+
""").strip()
|
97
|
+
# fmt: on
|
98
|
+
|
99
|
+
assert self.console.log.lines == ["my log 1", "my log 2"]
|
100
|
+
assert self.console.debug.lines == ["my debug"]
|
101
|
+
|
102
|
+
def test_check_js_errors_simple(self):
|
103
|
+
doc = """
|
104
|
+
<html>
|
105
|
+
<body>
|
106
|
+
<script>throw new Error('this is an error');</script>
|
107
|
+
</body>
|
108
|
+
</html>
|
109
|
+
"""
|
110
|
+
self.writefile("mytest.html", doc)
|
111
|
+
self.goto("mytest.html")
|
112
|
+
with pytest.raises(PageErrors) as exc:
|
113
|
+
self.check_js_errors()
|
114
|
+
# check that the exception message contains the error message and the
|
115
|
+
# stack trace
|
116
|
+
msg = str(exc.value)
|
117
|
+
expected = textwrap.dedent(
|
118
|
+
f"""
|
119
|
+
JS errors found: 1
|
120
|
+
Error: this is an error
|
121
|
+
at {self.http_server_addr}/mytest.html:.*
|
122
|
+
"""
|
123
|
+
).strip()
|
124
|
+
assert re.search(expected, msg)
|
125
|
+
#
|
126
|
+
# after a call to check_js_errors, the errors are cleared
|
127
|
+
self.check_js_errors()
|
128
|
+
#
|
129
|
+
# JS exceptions are also available in self.console.js_error
|
130
|
+
assert self.console.js_error.lines[0].startswith("Error: this is an error")
|
131
|
+
|
132
|
+
def test_check_js_errors_expected(self):
|
133
|
+
doc = """
|
134
|
+
<html>
|
135
|
+
<body>
|
136
|
+
<script>throw new Error('this is an error');</script>
|
137
|
+
</body>
|
138
|
+
</html>
|
139
|
+
"""
|
140
|
+
self.writefile("mytest.html", doc)
|
141
|
+
self.goto("mytest.html")
|
142
|
+
self.check_js_errors("this is an error")
|
143
|
+
|
144
|
+
def test_check_js_errors_expected_but_didnt_raise(self):
|
145
|
+
doc = """
|
146
|
+
<html>
|
147
|
+
<body>
|
148
|
+
<script>throw new Error('this is an error 2');</script>
|
149
|
+
<script>throw new Error('this is an error 4');</script>
|
150
|
+
</body>
|
151
|
+
</html>
|
152
|
+
"""
|
153
|
+
self.writefile("mytest.html", doc)
|
154
|
+
self.goto("mytest.html")
|
155
|
+
with pytest.raises(PageErrorsDidNotRaise) as exc:
|
156
|
+
self.check_js_errors(
|
157
|
+
"this is an error 1",
|
158
|
+
"this is an error 2",
|
159
|
+
"this is an error 3",
|
160
|
+
"this is an error 4",
|
161
|
+
)
|
162
|
+
#
|
163
|
+
msg = str(exc.value)
|
164
|
+
expected = textwrap.dedent(
|
165
|
+
"""
|
166
|
+
The following JS errors were expected but could not be found:
|
167
|
+
- this is an error 1
|
168
|
+
- this is an error 3
|
169
|
+
"""
|
170
|
+
).strip()
|
171
|
+
assert re.search(expected, msg)
|
172
|
+
|
173
|
+
def test_check_js_errors_multiple(self):
|
174
|
+
doc = """
|
175
|
+
<html>
|
176
|
+
<body>
|
177
|
+
<script>throw new Error('error 1');</script>
|
178
|
+
<script>throw new Error('error 2');</script>
|
179
|
+
</body>
|
180
|
+
</html>
|
181
|
+
"""
|
182
|
+
self.writefile("mytest.html", doc)
|
183
|
+
self.goto("mytest.html")
|
184
|
+
with pytest.raises(PageErrors) as exc:
|
185
|
+
self.check_js_errors()
|
186
|
+
#
|
187
|
+
msg = str(exc.value)
|
188
|
+
expected = textwrap.dedent(
|
189
|
+
"""
|
190
|
+
JS errors found: 2
|
191
|
+
Error: error 1
|
192
|
+
at https://fake_server/mytest.html:.*
|
193
|
+
Error: error 2
|
194
|
+
at https://fake_server/mytest.html:.*
|
195
|
+
"""
|
196
|
+
).strip()
|
197
|
+
assert re.search(expected, msg)
|
198
|
+
#
|
199
|
+
# check that errors are cleared
|
200
|
+
self.check_js_errors()
|
201
|
+
|
202
|
+
def test_check_js_errors_some_expected_but_others_not(self):
|
203
|
+
doc = """
|
204
|
+
<html>
|
205
|
+
<body>
|
206
|
+
<script>throw new Error('expected 1');</script>
|
207
|
+
<script>throw new Error('NOT expected 2');</script>
|
208
|
+
<script>throw new Error('expected 3');</script>
|
209
|
+
<script>throw new Error('NOT expected 4');</script>
|
210
|
+
</body>
|
211
|
+
</html>
|
212
|
+
"""
|
213
|
+
self.writefile("mytest.html", doc)
|
214
|
+
self.goto("mytest.html")
|
215
|
+
with pytest.raises(PageErrors) as exc:
|
216
|
+
self.check_js_errors("expected 1", "expected 3")
|
217
|
+
#
|
218
|
+
msg = str(exc.value)
|
219
|
+
expected = textwrap.dedent(
|
220
|
+
"""
|
221
|
+
JS errors found: 2
|
222
|
+
Error: NOT expected 2
|
223
|
+
at https://fake_server/mytest.html:.*
|
224
|
+
Error: NOT expected 4
|
225
|
+
at https://fake_server/mytest.html:.*
|
226
|
+
"""
|
227
|
+
).strip()
|
228
|
+
assert re.search(expected, msg)
|
229
|
+
|
230
|
+
def test_check_js_errors_expected_not_found_but_other_errors(self):
|
231
|
+
doc = """
|
232
|
+
<html>
|
233
|
+
<body>
|
234
|
+
<script>throw new Error('error 1');</script>
|
235
|
+
<script>throw new Error('error 2');</script>
|
236
|
+
</body>
|
237
|
+
</html>
|
238
|
+
"""
|
239
|
+
self.writefile("mytest.html", doc)
|
240
|
+
self.goto("mytest.html")
|
241
|
+
with pytest.raises(PageErrorsDidNotRaise) as exc:
|
242
|
+
self.check_js_errors("this is not going to be found")
|
243
|
+
#
|
244
|
+
msg = str(exc.value)
|
245
|
+
expected = textwrap.dedent(
|
246
|
+
"""
|
247
|
+
The following JS errors were expected but could not be found:
|
248
|
+
- this is not going to be found
|
249
|
+
---
|
250
|
+
The following JS errors were raised but not expected:
|
251
|
+
Error: error 1
|
252
|
+
at https://fake_server/mytest.html:.*
|
253
|
+
Error: error 2
|
254
|
+
at https://fake_server/mytest.html:.*
|
255
|
+
"""
|
256
|
+
).strip()
|
257
|
+
assert re.search(expected, msg)
|
258
|
+
|
259
|
+
def test_clear_js_errors(self):
|
260
|
+
doc = """
|
261
|
+
<html>
|
262
|
+
<body>
|
263
|
+
<script>throw new Error('this is an error');</script>
|
264
|
+
</body>
|
265
|
+
</html>
|
266
|
+
"""
|
267
|
+
self.writefile("mytest.html", doc)
|
268
|
+
self.goto("mytest.html")
|
269
|
+
self.clear_js_errors()
|
270
|
+
# self.check_js_errors does not raise, because the errors have been
|
271
|
+
# cleared
|
272
|
+
self.check_js_errors()
|
273
|
+
|
274
|
+
def test_wait_for_console_simple(self):
|
275
|
+
"""
|
276
|
+
Test that self.wait_for_console actually waits.
|
277
|
+
If it's buggy, the test will try to read self.console.log BEFORE the
|
278
|
+
log has been written and it will fail.
|
279
|
+
"""
|
280
|
+
doc = """
|
281
|
+
<html>
|
282
|
+
<body>
|
283
|
+
<script>
|
284
|
+
setTimeout(function() {
|
285
|
+
console.log('Page loaded!');
|
286
|
+
}, 100);
|
287
|
+
</script>
|
288
|
+
</body>
|
289
|
+
</html>
|
290
|
+
"""
|
291
|
+
self.writefile("mytest.html", doc)
|
292
|
+
self.goto("mytest.html")
|
293
|
+
# we use a timeout of 200ms to give plenty of time to the page to
|
294
|
+
# actually run the setTimeout callback
|
295
|
+
self.wait_for_console("Page loaded!", timeout=200)
|
296
|
+
assert self.console.log.lines[-1] == "Page loaded!"
|
297
|
+
|
298
|
+
def test_wait_for_console_timeout(self):
|
299
|
+
doc = """
|
300
|
+
<html>
|
301
|
+
<body>
|
302
|
+
</body>
|
303
|
+
</html>
|
304
|
+
"""
|
305
|
+
self.writefile("mytest.html", doc)
|
306
|
+
self.goto("mytest.html")
|
307
|
+
with pytest.raises(TimeoutError):
|
308
|
+
self.wait_for_console("This text will never be printed", timeout=200)
|
309
|
+
|
310
|
+
def test_wait_for_console_dont_wait_if_already_emitted(self):
|
311
|
+
"""
|
312
|
+
If the text is already on the console, wait_for_console() should return
|
313
|
+
immediately without waiting.
|
314
|
+
"""
|
315
|
+
doc = """
|
316
|
+
<html>
|
317
|
+
<body>
|
318
|
+
<script>
|
319
|
+
console.log('Hello world')
|
320
|
+
console.log('Page loaded!');
|
321
|
+
</script>
|
322
|
+
</body>
|
323
|
+
</html>
|
324
|
+
"""
|
325
|
+
self.writefile("mytest.html", doc)
|
326
|
+
self.goto("mytest.html")
|
327
|
+
self.wait_for_console("Page loaded!", timeout=200)
|
328
|
+
assert self.console.log.lines[-2] == "Hello world"
|
329
|
+
assert self.console.log.lines[-1] == "Page loaded!"
|
330
|
+
# the following call should return immediately without waiting
|
331
|
+
self.wait_for_console("Hello world", timeout=1)
|
332
|
+
|
333
|
+
def test_wait_for_console_exception_1(self):
|
334
|
+
"""
|
335
|
+
Test that if a JS exception is raised while waiting for the console, we
|
336
|
+
report the exception and not the timeout.
|
337
|
+
|
338
|
+
There are two main cases:
|
339
|
+
1. there is an exception and the console message does not appear
|
340
|
+
2. there is an exception but the console message appears anyway
|
341
|
+
|
342
|
+
This test checks for case 1. Case 2 is tested by
|
343
|
+
test_wait_for_console_exception_2
|
344
|
+
"""
|
345
|
+
# case 1: there is an exception and the console message does not appear
|
346
|
+
doc = """
|
347
|
+
<html>
|
348
|
+
<body>
|
349
|
+
<script>throw new Error('this is an error');</script>
|
350
|
+
</body>
|
351
|
+
</html>
|
352
|
+
"""
|
353
|
+
self.writefile("mytest.html", doc)
|
354
|
+
# "Page loaded!" will never appear, of course.
|
355
|
+
self.goto("mytest.html")
|
356
|
+
with pytest.raises(PageErrors) as exc:
|
357
|
+
self.wait_for_console("Page loaded!", timeout=200)
|
358
|
+
assert "this is an error" in str(exc.value)
|
359
|
+
assert isinstance(exc.value.__context__, TimeoutError)
|
360
|
+
#
|
361
|
+
# if we use check_js_errors=False, the error are ignored, but we get the
|
362
|
+
# Timeout anyway
|
363
|
+
self.goto("mytest.html")
|
364
|
+
with pytest.raises(TimeoutError):
|
365
|
+
self.wait_for_console("Page loaded!", timeout=200, check_js_errors=False)
|
366
|
+
# we still got a PageErrors, so we need to manually clear it, else the
|
367
|
+
# test fails at teardown
|
368
|
+
self.clear_js_errors()
|
369
|
+
|
370
|
+
def test_wait_for_console_exception_2(self):
|
371
|
+
"""
|
372
|
+
See the description in test_wait_for_console_exception_1.
|
373
|
+
"""
|
374
|
+
# case 2: there is an exception, but the console message appears
|
375
|
+
doc = """
|
376
|
+
<html>
|
377
|
+
<body>
|
378
|
+
<script>
|
379
|
+
setTimeout(function() {
|
380
|
+
console.log('Page loaded!');
|
381
|
+
}, 100);
|
382
|
+
throw new Error('this is an error');
|
383
|
+
</script>
|
384
|
+
</body>
|
385
|
+
</html>
|
386
|
+
"""
|
387
|
+
self.writefile("mytest.html", doc)
|
388
|
+
self.goto("mytest.html")
|
389
|
+
with pytest.raises(PageErrors) as exc:
|
390
|
+
self.wait_for_console("Page loaded!", timeout=200)
|
391
|
+
assert "this is an error" in str(exc.value)
|
392
|
+
#
|
393
|
+
# with check_js_errors=False, the Error is ignored and the
|
394
|
+
# wait_for_console succeeds
|
395
|
+
self.goto("mytest.html")
|
396
|
+
self.wait_for_console("Page loaded!", timeout=200, check_js_errors=False)
|
397
|
+
# clear the errors, else the test fails at teardown
|
398
|
+
self.clear_js_errors()
|
399
|
+
|
400
|
+
def test_wait_for_console_match_substring(self):
|
401
|
+
doc = """
|
402
|
+
<html>
|
403
|
+
<body>
|
404
|
+
<script>
|
405
|
+
console.log('Foo Bar Baz');
|
406
|
+
</script>
|
407
|
+
</body>
|
408
|
+
</html>
|
409
|
+
"""
|
410
|
+
self.writefile("mytest.html", doc)
|
411
|
+
self.goto("mytest.html")
|
412
|
+
with pytest.raises(TimeoutError):
|
413
|
+
self.wait_for_console("Bar", timeout=200)
|
414
|
+
#
|
415
|
+
self.wait_for_console("Bar", timeout=200, match_substring=True)
|
416
|
+
assert self.console.log.lines[-1] == "Foo Bar Baz"
|
417
|
+
|
418
|
+
def test_iter_locator(self):
|
419
|
+
doc = """
|
420
|
+
<html>
|
421
|
+
<body>
|
422
|
+
<div>foo</div>
|
423
|
+
<div>bar</div>
|
424
|
+
<div>baz</div>
|
425
|
+
</body>
|
426
|
+
</html>
|
427
|
+
"""
|
428
|
+
self.writefile("mytest.html", doc)
|
429
|
+
self.goto("mytest.html")
|
430
|
+
divs = self.page.locator("div")
|
431
|
+
assert divs.count() == 3
|
432
|
+
texts = [el.inner_text() for el in self.iter_locator(divs)]
|
433
|
+
assert texts == ["foo", "bar", "baz"]
|
434
|
+
|
435
|
+
def test_smartrouter_cache(self):
|
436
|
+
if self.router is None:
|
437
|
+
pytest.skip("Cannot test SmartRouter with --dev")
|
438
|
+
|
439
|
+
# this is not an image but who cares, I just want the browser to make
|
440
|
+
# an HTTP request
|
441
|
+
URL = "https://raw.githubusercontent.com/pyscript/pyscript/main/README.md"
|
442
|
+
doc = f"""
|
443
|
+
<html>
|
444
|
+
<body>
|
445
|
+
<img src="{URL}">
|
446
|
+
</body>
|
447
|
+
</html>
|
448
|
+
"""
|
449
|
+
self.writefile("mytest.html", doc)
|
450
|
+
#
|
451
|
+
self.router.clear_cache(URL)
|
452
|
+
self.goto("mytest.html")
|
453
|
+
assert self.router.requests == [
|
454
|
+
(200, "fake_server", "https://fake_server/mytest.html"),
|
455
|
+
(200, "NETWORK", URL),
|
456
|
+
]
|
457
|
+
#
|
458
|
+
# let's visit the page again, now it should be cached
|
459
|
+
self.goto("mytest.html")
|
460
|
+
assert self.router.requests == [
|
461
|
+
# 1st visit
|
462
|
+
(200, "fake_server", "https://fake_server/mytest.html"),
|
463
|
+
(200, "NETWORK", URL),
|
464
|
+
# 2nd visit
|
465
|
+
(200, "fake_server", "https://fake_server/mytest.html"),
|
466
|
+
(200, "CACHED", URL),
|
467
|
+
]
|
468
|
+
|
469
|
+
def test_404(self):
|
470
|
+
"""
|
471
|
+
Test that we capture a 404 in loading a page that does not exist.
|
472
|
+
"""
|
473
|
+
self.goto("this_url_does_not_exist.html")
|
474
|
+
assert [
|
475
|
+
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
476
|
+
] == self.console.all.lines
|
477
|
+
|
478
|
+
def test__pyscript_format_inject_execution_thread(self):
|
479
|
+
"""
|
480
|
+
This is slightly different than other tests: it doesn't use playwright, it
|
481
|
+
just tests that our own internal helper works
|
482
|
+
"""
|
483
|
+
doc = self._pyscript_format("<b>Hello</b>", execution_thread="main")
|
484
|
+
cfg = self._parse_py_config(doc)
|
485
|
+
assert cfg == {"execution_thread": "main"}
|
486
|
+
|
487
|
+
def test__pyscript_format_modify_existing_py_config(self):
|
488
|
+
src = """
|
489
|
+
<py-config>
|
490
|
+
hello = 42
|
491
|
+
</py-config>
|
492
|
+
"""
|
493
|
+
doc = self._pyscript_format(src, execution_thread="main")
|
494
|
+
cfg = self._parse_py_config(doc)
|
495
|
+
assert cfg == {"execution_thread": "main", "hello": 42}
|