Skip to content

Commit c985bd1

Browse files
committed
Improve serialization and tests
1 parent 5fee527 commit c985bd1

File tree

4 files changed

+142
-30
lines changed

4 files changed

+142
-30
lines changed

tests/test_serialization.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
2+
import webview
3+
import json
4+
from .util import run_test
5+
import pytest
6+
7+
test_values = [
8+
('int', 1),
9+
('string', 'test'),
10+
('null_value', None),
11+
('object', {'key1': 'value', 'key2': 420}),
12+
('array', [1, 2, 3]),
13+
('mixed', {'key1': 'value', 'key2': [ 1, 2, {'id': 2}], 'nullValue': None}),
14+
('boolean', True),
15+
]
16+
17+
test_value_string = '\n'.join([ f"var {name} = {json.dumps(value)}" for name, value in test_values])
18+
19+
HTML = f"""
20+
<!DOCTYPE html>
21+
<html>
22+
<head>
23+
24+
</head>
25+
<body style="width: 100%; height: 100vh; display: flex;">
26+
27+
<script>
28+
{test_value_string}
29+
var circular = {{key: "test"}}
30+
circular.circular = circular
31+
var testObject = {{id: 1}}
32+
var nonCircular = [testObject, testObject]
33+
34+
var nodes = document.getElementsByTagName("div")
35+
</script>
36+
<div>1</div>
37+
<div style="font-family: Helvetica, Arial, sans-serif; font-size: 34px; font-style: italic; font-weight: 800; width: 100%; display: flex; justify-content; center; align-items: center;">
38+
<h1>THIS IS ONLY A TEST</h1>
39+
</h1></div>
40+
<div>3</div>
41+
</body>
42+
</html>
43+
"""
44+
45+
def test_basic_serialization():
46+
window = webview.create_window('Basic serialization test', html=HTML)
47+
run_test(webview, window, serialization, debug=True)
48+
49+
50+
def test_circular_serialization():
51+
window = webview.create_window('Circular reference test', html=HTML)
52+
run_test(webview, window, circular_reference)
53+
54+
55+
def test_dom_serialization():
56+
window = webview.create_window('DOM serialization test', html=HTML)
57+
run_test(webview, window, dom_serialization, debug=True)
58+
59+
60+
def serialization(window):
61+
for name, expected_value in test_values:
62+
result = window.evaluate_js(name)
63+
assert result == expected_value
64+
65+
def circular_reference(window):
66+
result = window.evaluate_js('circular')
67+
assert result == {'key': 'test', 'circular': '[Circular Reference]'}
68+
69+
result = window.evaluate_js('nonCircular')
70+
assert result == [{'id': 1}, {'id': 1}]
71+
72+
73+
def dom_serialization(window):
74+
result = window.evaluate_js('nodes')
75+
assert len(result) == 3
76+
assert result[0]['innerText'] == '1'
77+
assert result[1]['innerText'].strip() == 'THIS IS ONLY A TEST'
78+
assert result[2]['innerText'] == '3'

tests/test_set_title.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ def test_set_title():
99

1010

1111
def set_title(window):
12-
window.set_title('New title')
12+
assert window.title == 'Set title test'
13+
window.title = 'New title'
14+
15+
assert window.title == 'New title'

tests/util.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import traceback
77
from multiprocessing import Queue
8+
from typing import Any, Callable, Dict, Iterable, Optional
89
from uuid import uuid4
910

1011
import pytest
@@ -13,11 +14,34 @@
1314

1415

1516
def run_test(
16-
webview, window, thread_func=None, param=None, start_args={}, no_destroy=False, destroy_delay=0, debug=False
17-
):
17+
webview: Any,
18+
window: Any,
19+
thread_func: Optional[Callable] = None,
20+
param: Iterable = (),
21+
start_args: Dict[str, Any] = {},
22+
no_destroy: bool = False,
23+
destroy_delay: float = 0,
24+
debug: bool = False
25+
) -> None:
26+
""""
27+
A test running function that creates a window and runs a thread function in it. Test logic is to be placed in the
28+
thread function. The function will wait for the thread function to finish and then destroy the window. If the thread
29+
function raises an exception, the test will fail and the exception will be printed.
30+
31+
:param webview: webview module
32+
:param window: window instance created with webview.create_window
33+
:param thread_func: function to run in the window.
34+
:param param: positional arguments to pass to the thread function
35+
:param start_args: keyword arguments to pass to webview.start
36+
:param no_destroy: flag indicating whether to destroy the window after the thread function finishes (default: False).
37+
If set to True, the window will not be destroyed and the test will not finish until the window is closed manually.
38+
:param destroy_delay: delay in seconds before destroying the window (default: 0)
39+
:param debug: flag indicating whether to enable debug mode (default: False)
40+
41+
"""
1842
__tracebackhide__ = True
1943
try:
20-
queue = Queue()
44+
queue: Queue = Queue()
2145

2246
if debug:
2347
start_args = {**start_args, 'debug': True}
@@ -71,10 +95,11 @@ def _create_window(
7195
):
7296
def thread():
7397
try:
98+
logger.info('Thread started')
7499
take_screenshot()
75100
move_mouse_cocoa()
76101
if thread_func:
77-
thread_func(window)
102+
thread_func(window, *thread_param)
78103

79104
destroy_event.set()
80105
except Exception as e:
@@ -86,7 +111,7 @@ def thread():
86111
args = (thread_param,) if thread_param else ()
87112
destroy_event = _destroy_window(webview, window, destroy_delay)
88113

89-
t = threading.Thread(target=thread, args=args)
114+
t = threading.Thread(target=thread)
90115
t.start()
91116

92117
webview.start(**start_args)

webview/js/api.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -115,48 +115,54 @@
115115
)
116116
}
117117
118-
function serialize(obj, depth=0, visited=new WeakSet()) {
118+
function serialize(obj, ancestors=[]) {
119119
try {
120120
if (obj instanceof Node) return pywebview.domJSON.toJSON(obj, { metadata: false, serialProperties: true });
121121
if (obj instanceof Window) return 'Window';
122122
123-
if (visited.has(obj)) {
124-
return '[Circular Reference]';
123+
var boundSerialize = serialize.bind(obj);
124+
125+
if (typeof obj !== "object" || obj === null) {
126+
return obj;
125127
}
126128
127-
if (typeof obj === 'object' && obj !== null) {
128-
visited.add(obj);
129+
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
130+
ancestors.pop();
131+
}
129132
130-
if (isArrayLike(obj)) {
131-
obj = tryConvertToArray(obj);
132-
}
133+
if (ancestors.includes(obj)) {
134+
return "[Circular Reference]";
135+
}
136+
ancestors.push(obj);
133137
134-
if (Array.isArray(obj)) {
135-
const arr = obj.map(value => serialize(value, depth + 1, visited));
136-
visited.delete(obj);
137-
return arr;
138-
}
138+
if (isArrayLike(obj)) {
139+
obj = tryConvertToArray(obj);
140+
}
139141
140-
const newObj = {};
141-
for (const key in obj) {
142-
if (typeof obj === 'function') {
143-
continue;
144-
}
145-
newObj[key] = serialize(obj[key], depth + 1, visited);
142+
if (Array.isArray(obj)) {
143+
const arr = obj.map(value => boundSerialize(value, ancestors));
144+
return arr;
145+
}
146+
147+
const newObj = {};
148+
for (const key in obj) {
149+
if (typeof obj === 'function') {
150+
continue;
146151
}
147-
visited.delete(obj);
148-
return newObj;
152+
newObj[key] = boundSerialize(obj[key], ancestors);
149153
}
154+
return newObj;
150155
151-
return obj;
152156
} catch (e) {
153157
console.error(e)
154158
return e.toString();
155159
}
156160
}
157161
158-
return JSON.stringify(serialize(obj));
159-
},
162+
var _serialize = serialize.bind(null);
163+
164+
return JSON.stringify(_serialize(obj));
165+
},
160166
161167
_getNodeId: function (element) {
162168
if (!element) {

0 commit comments

Comments
 (0)