Skip to content

Commit ce464f9

Browse files
dbrun3nathanfallet
andauthored
fix: re-add nodriver evaluate serialization options (#190)
* fix: readd nodriver evaluate serialization options * chore: update changelog * added tests to validate bug has been fixed, complex_object.html for testing complex evaluate cases, formatting * more tests * fix value_modes test * fix linting and typing --------- Co-authored-by: Nathan Fallet <contact@nathanfallet.me>
1 parent 03e3403 commit ce464f9

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Re-add nodriver evaluate serialization options for improved JavaScript evaluation @dbrun3
13+
1214
### Added
1315

1416
- Fix: allow reset expect and intercept @nathanfallet

tests/core/test_tab.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,67 @@ async def test_intercept_with_reload(browser: zd.Browser) -> None:
289289
assert body is not None
290290
# original_response = loads(body)
291291
# assert original_response["name"] == "Zendriver"
292+
293+
294+
async def test_evaluate_complex_object_no_error(browser: zd.Browser) -> None:
295+
tab = await browser.get(sample_file("complex_object.html"))
296+
await tab.wait_for_ready_state("complete")
297+
298+
result = await tab.evaluate("document.querySelector('body:not(.no-js)')")
299+
assert result is not None
300+
301+
# This is similar to the original failing case but more likely to trigger the error
302+
body_with_complex_refs = await tab.evaluate("document.body")
303+
assert body_with_complex_refs is not None
304+
305+
306+
async def test_evaluate_return_by_value_modes(browser: zd.Browser) -> None:
307+
tab = await browser.get(sample_file("complex_object.html"))
308+
await tab.wait_for_ready_state("complete")
309+
310+
expression = "document.querySelector('body:not(.no-js)')"
311+
312+
result_by_value = await tab.evaluate(expression, return_by_value=True)
313+
assert result_by_value is not None
314+
315+
result_false = await tab.evaluate(expression, return_by_value=False)
316+
assert (
317+
result_false is not None
318+
) # Should return the deep serialized value, not a tuple
319+
320+
321+
async def test_evaluate_stress_test_complex_objects(browser: zd.Browser) -> None:
322+
tab = await browser.get(sample_file("complex_object.html"))
323+
await tab.wait_for_ready_state("complete")
324+
325+
# Test various DOM queries that could trigger reference chain issues
326+
# Each test case is a tuple of (expression, expected_type_or_validator)
327+
test_cases = [
328+
("document.querySelector('body:not(.no-js)')", lambda x: x is not None),
329+
("document.documentElement", lambda x: x is not None),
330+
("document.querySelector('*')", lambda x: x is not None),
331+
("document.body.parentElement", lambda x: x is not None),
332+
("document.getElementById('content')", lambda x: x is not None),
333+
(
334+
"document.body.complexStructure ? 'has complex structure' : 'no structure'",
335+
str,
336+
),
337+
("document.readyState", str),
338+
("navigator.userAgent", str),
339+
("window.location.href", str),
340+
("document.title", str),
341+
]
342+
343+
for expression, validator in test_cases:
344+
result = await tab.evaluate(expression)
345+
# Verify the result is usable and matches expected type/validation
346+
if callable(validator):
347+
assert validator(
348+
result
349+
), f"Result validation failed for '{expression}': {result}"
350+
elif isinstance(validator, type):
351+
assert isinstance(
352+
result, validator
353+
), f"Expected {validator} for '{expression}', got {type(result)}: {result}"
354+
else:
355+
raise ValueError("Validator must be a type or callable")
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Complex Object Test</title>
8+
</head>
9+
10+
<body class="no-js">
11+
<div id="content">
12+
<h1>Complex Object Chain Test</h1>
13+
<p>This page creates a complex object structure that may cause "Object reference chain is too long" errors.</p>
14+
</div>
15+
16+
<script>
17+
// Remove no-js class immediately
18+
document.body.classList.remove('no-js');
19+
20+
// The key insight is that DOM elements themselves can have properties added
21+
// that create circular references when Chrome tries to serialize them
22+
23+
// Add complex circular references directly to DOM elements
24+
document.body.complexRef = document;
25+
document.body.windowRef = window;
26+
document.body.self = document.body;
27+
28+
// Create a complex object and attach it to DOM elements
29+
const createComplexStructure = () => {
30+
const structure = {};
31+
structure.self = structure;
32+
structure.body = document.body;
33+
structure.document = document;
34+
structure.window = window;
35+
36+
// Create deep nesting with DOM references
37+
let current = structure;
38+
for (let i = 0; i < 100; i++) {
39+
current.next = {
40+
level: i,
41+
parent: current,
42+
body: document.body,
43+
document: document,
44+
window: window,
45+
elements: Array.from(document.querySelectorAll('*'))
46+
};
47+
current = current.next;
48+
}
49+
current.back_to_start = structure;
50+
51+
return structure;
52+
};
53+
54+
const complexStructure = createComplexStructure();
55+
56+
// Attach the complex structure to DOM elements
57+
document.body.complexStructure = complexStructure;
58+
document.documentElement.complexStructure = complexStructure;
59+
60+
// Add references to all elements
61+
Array.from(document.querySelectorAll('*')).forEach((el, i) => {
62+
el.complexRef = complexStructure;
63+
el.index = i;
64+
el.bodyRef = document.body;
65+
el.documentRef = document;
66+
el.windowRef = window;
67+
});
68+
69+
// Create circular references between elements
70+
const allElements = Array.from(document.querySelectorAll('*'));
71+
allElements.forEach((el, i) => {
72+
el.nextElement = allElements[(i + 1) % allElements.length];
73+
el.prevElement = allElements[(i - 1 + allElements.length) % allElements.length];
74+
el.allElements = allElements;
75+
});
76+
77+
// Add properties to the body element that create deep circular references
78+
document.body.deepRef = {
79+
level1: { level2: { level3: { level4: { level5: { body: document.body } } } } }
80+
};
81+
document.body.deepRef.level1.level2.level3.level4.level5.back = document.body.deepRef;
82+
83+
console.log('Complex DOM structure with circular references created');
84+
</script>
85+
</body>
86+
87+
</html>

zendriver/core/tab.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,20 @@ async def evaluate(
718718
| None
719719
| typing.Tuple[cdp.runtime.RemoteObject, cdp.runtime.ExceptionDetails | None]
720720
):
721+
ser = cdp.runtime.SerializationOptions(
722+
serialization="deep",
723+
max_depth=10,
724+
additional_parameters={"maxNodeDepth": 10, "includeShadowTree": "all"},
725+
)
726+
721727
remote_object, errors = await self.send(
722728
cdp.runtime.evaluate(
723729
expression=expression,
724730
user_gesture=True,
725731
await_promise=await_promise,
726732
return_by_value=return_by_value,
727733
allow_unsafe_eval_blocked_by_csp=True,
734+
serialization_options=ser,
728735
)
729736
)
730737
if errors:
@@ -734,6 +741,9 @@ async def evaluate(
734741
if return_by_value:
735742
if remote_object.value:
736743
return remote_object.value
744+
else:
745+
if remote_object.deep_serialized_value:
746+
return remote_object.deep_serialized_value.value
737747

738748
return remote_object, errors
739749

0 commit comments

Comments
 (0)