Skip to content

Commit 41518b5

Browse files
committed
fix tests for index is default key
1 parent 8ec39c5 commit 41518b5

File tree

3 files changed

+140
-115
lines changed

3 files changed

+140
-115
lines changed

src/idom/testing.py

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Generic,
1515
Iterator,
1616
List,
17+
NoReturn,
1718
Optional,
1819
Tuple,
1920
Type,
@@ -173,8 +174,12 @@ def __exit__(
173174
return None
174175

175176

177+
class LogAssertionError(AssertionError):
178+
"""An assertion error raised in relation to log messages."""
179+
180+
176181
@contextmanager
177-
def assert_idom_logged(
182+
def assert_idom_did_log(
178183
match_message: str = "",
179184
error_type: type[Exception] | None = None,
180185
match_error: str = "",
@@ -192,11 +197,13 @@ def assert_idom_logged(
192197
error_pattern = re.compile(match_error)
193198

194199
try:
195-
with capture_idom_logs() as handler:
200+
with capture_idom_logs(use_existing=clear_matched_records) as log_records:
196201
yield None
197-
finally:
202+
except Exception:
203+
raise
204+
else:
198205
found = False
199-
for record in list(handler.records):
206+
for record in list(log_records):
200207
if (
201208
# record message matches
202209
message_pattern.findall(record.getMessage())
@@ -222,36 +229,79 @@ def assert_idom_logged(
222229
):
223230
found = True
224231
if clear_matched_records:
225-
handler.records.remove(record)
232+
log_records.remove(record)
226233

227234
if not found: # pragma: no cover
228-
conditions = []
229-
if match_message:
230-
conditions.append(f"log message pattern {match_message!r}")
231-
if error_type:
232-
conditions.append(f"exception type {error_type}")
233-
if match_error:
234-
conditions.append(f"error message pattern {match_error!r}")
235-
raise AssertionError(
236-
"Could not find a log record matching the given "
237-
+ " and ".join(conditions)
235+
_raise_log_message_error(
236+
"Could not find a log record matching the given",
237+
match_message,
238+
error_type,
239+
match_error,
238240
)
239241

240242

241243
@contextmanager
242-
def capture_idom_logs() -> Iterator[_LogRecordCaptor]:
243-
"""Capture logs from IDOM"""
244-
if _LOG_RECORD_CAPTOR_SINGLTON in ROOT_LOGGER.handlers:
245-
# this is being handled by an outer capture context
246-
yield _LOG_RECORD_CAPTOR_SINGLTON
247-
return None
244+
def assert_idom_did_not_log(
245+
match_message: str = "",
246+
error_type: type[Exception] | None = None,
247+
match_error: str = "",
248+
clear_matched_records: bool = False,
249+
) -> Iterator[None]:
250+
"""Assert the inverse of :func:`assert_idom_did_log`"""
251+
try:
252+
with assert_idom_did_log(
253+
match_message, error_type, match_error, clear_matched_records
254+
):
255+
yield None
256+
except LogAssertionError:
257+
pass
258+
else:
259+
_raise_log_message_error(
260+
"Did find a log record matching the given",
261+
match_message,
262+
error_type,
263+
match_error,
264+
)
265+
266+
267+
def _raise_log_message_error(
268+
prefix: str,
269+
match_message: str = "",
270+
error_type: type[Exception] | None = None,
271+
match_error: str = "",
272+
) -> NoReturn:
273+
conditions = []
274+
if match_message:
275+
conditions.append(f"log message pattern {match_message!r}")
276+
if error_type:
277+
conditions.append(f"exception type {error_type}")
278+
if match_error:
279+
conditions.append(f"error message pattern {match_error!r}")
280+
raise LogAssertionError(prefix + " " + " and ".join(conditions))
281+
248282

249-
ROOT_LOGGER.addHandler(_LOG_RECORD_CAPTOR_SINGLTON)
283+
@contextmanager
284+
def capture_idom_logs(use_existing: bool = False) -> Iterator[list[logging.LogRecord]]:
285+
"""Capture logs from IDOM
286+
287+
Parameters:
288+
use_existing:
289+
If already inside an existing capture context yield the same list of logs.
290+
This is useful if you need to mutate the list of logs to affect behavior in
291+
the outer context.
292+
"""
293+
if use_existing:
294+
for handler in reversed(ROOT_LOGGER.handlers):
295+
if isinstance(handler, _LogRecordCaptor):
296+
yield handler.records
297+
return None
298+
299+
handler = _LogRecordCaptor()
300+
ROOT_LOGGER.addHandler(handler)
250301
try:
251-
yield _LOG_RECORD_CAPTOR_SINGLTON
302+
yield handler.records
252303
finally:
253-
ROOT_LOGGER.removeHandler(_LOG_RECORD_CAPTOR_SINGLTON)
254-
_LOG_RECORD_CAPTOR_SINGLTON.records = []
304+
ROOT_LOGGER.removeHandler(handler)
255305

256306

257307
class _LogRecordCaptor(logging.NullHandler):

tests/test_core/test_hooks.py

Lines changed: 50 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import idom
77
from idom.core.dispatcher import render_json_patch
88
from idom.core.hooks import LifeCycleHook
9-
from idom.testing import HookCatcher
9+
from idom.testing import HookCatcher, assert_idom_did_log
1010
from tests.general_utils import assert_same_items
1111

1212

@@ -79,89 +79,68 @@ def SimpleStatefulComponent():
7979
assert first_hook is h
8080

8181

82-
def test_use_state_with_constructor(driver, display, driver_wait):
82+
async def test_use_state_with_constructor():
8383
constructor_call_count = idom.Ref(0)
8484

85+
set_outer_state = idom.Ref()
86+
set_inner_key = idom.Ref()
87+
set_inner_state = idom.Ref()
88+
8589
def make_default():
8690
constructor_call_count.current += 1
8791
return 0
8892

8993
@idom.component
9094
def Outer():
91-
hook = idom.hooks.current_hook()
92-
93-
async def on_click(event):
94-
hook.schedule_render()
95-
96-
return idom.html.div(
97-
idom.html.button(
98-
{"onClick": on_click, "id": "outer"}, "update outer (rerun constructor)"
99-
),
100-
Inner(),
101-
)
95+
state, set_outer_state.current = idom.use_state(0)
96+
inner_key, set_inner_key.current = idom.use_state("first")
97+
return idom.html.div(state, Inner(key=inner_key))
10298

10399
@idom.component
104100
def Inner():
105-
count, set_count = idom.hooks.use_state(make_default)
101+
state, set_inner_state.current = idom.use_state(make_default)
102+
return idom.html.div(state)
106103

107-
async def on_click(event):
108-
set_count(count + 1)
109-
110-
return idom.html.div(
111-
idom.html.button(
112-
{"onClick": on_click, "id": "inner"},
113-
"update inner with state constructor",
114-
),
115-
idom.html.p({"id": "count-view"}, count),
116-
)
117-
118-
display(Outer)
104+
with idom.Layout(Outer()) as layout:
105+
await layout.render()
119106

120-
outer = driver.find_element("id", "outer")
121-
inner = driver.find_element("id", "inner")
122-
count = driver.find_element("id", "count-view")
107+
assert constructor_call_count.current == 1
123108

124-
driver_wait.until(lambda d: constructor_call_count.current == 1)
125-
driver_wait.until(lambda d: count.get_attribute("innerHTML") == "0")
109+
set_outer_state.current(1)
110+
await layout.render()
126111

127-
inner.click()
112+
assert constructor_call_count.current == 1
128113

129-
driver_wait.until(lambda d: constructor_call_count.current == 1)
130-
driver_wait.until(lambda d: count.get_attribute("innerHTML") == "1")
114+
set_inner_state.current(1)
115+
await layout.render()
131116

132-
outer.click()
117+
assert constructor_call_count.current == 1
133118

134-
driver_wait.until(lambda d: constructor_call_count.current == 2)
135-
driver_wait.until(lambda d: count.get_attribute("innerHTML") == "0")
119+
set_inner_key.current("second")
120+
await layout.render()
136121

137-
inner.click()
122+
assert constructor_call_count.current == 2
138123

139-
driver_wait.until(lambda d: constructor_call_count.current == 2)
140-
driver_wait.until(lambda d: count.get_attribute("innerHTML") == "1")
141124

125+
async def test_set_state_with_reducer_instead_of_value():
126+
count = idom.Ref()
127+
set_count = idom.Ref()
142128

143-
def test_set_state_with_reducer_instead_of_value(driver, display):
144129
def increment(count):
145130
return count + 1
146131

147132
@idom.component
148133
def Counter():
149-
count, set_count = idom.hooks.use_state(0)
150-
return idom.html.button(
151-
{
152-
"id": "counter",
153-
"onClick": lambda event: set_count(increment),
154-
},
155-
f"Count: {count}",
156-
)
157-
158-
display(Counter)
134+
count.current, set_count.current = idom.hooks.use_state(0)
135+
return idom.html.div(count.current)
159136

160-
client_counter = driver.find_element("id", "counter")
137+
with idom.Layout(Counter()) as layout:
138+
await layout.render()
161139

162-
for i in range(3):
163-
assert client_counter.get_attribute("innerHTML") == f"Count: {i}"
164-
client_counter.click()
140+
for i in range(4):
141+
assert count.current == i
142+
set_count.current(increment)
143+
await layout.render()
165144

166145

167146
def test_set_state_checks_identity_not_equality(driver, display, driver_wait):
@@ -356,15 +335,15 @@ def cleanup():
356335

357336

358337
async def test_use_effect_cleanup_occurs_on_will_unmount():
359-
outer_component_hook = HookCatcher()
338+
set_key = idom.Ref()
360339
component_did_render = idom.Ref(False)
361340
cleanup_triggered = idom.Ref(False)
362341
cleanup_triggered_before_next_render = idom.Ref(False)
363342

364343
@idom.component
365-
@outer_component_hook.capture
366344
def OuterComponent():
367-
return ComponentWithEffect()
345+
key, set_key.current = idom.use_state("first")
346+
return ComponentWithEffect(key=key)
368347

369348
@idom.component
370349
def ComponentWithEffect():
@@ -387,7 +366,7 @@ def cleanup():
387366

388367
assert not cleanup_triggered.current
389368

390-
outer_component_hook.latest.schedule_render()
369+
set_key.current("second")
391370
await layout.render()
392371

393372
assert cleanup_triggered.current
@@ -592,13 +571,13 @@ def bad_cleanup():
592571
assert re.match("Post-render effect .*?", first_log_line)
593572

594573

595-
async def test_error_in_effect_pre_unmount_cleanup_is_gracefully_handled(caplog):
596-
outer_component_hook = HookCatcher()
574+
async def test_error_in_effect_pre_unmount_cleanup_is_gracefully_handled():
575+
set_key = idom.Ref()
597576

598577
@idom.component
599-
@outer_component_hook.capture
600578
def OuterComponent():
601-
return ComponentWithEffect()
579+
key, set_key.current = idom.use_state("first")
580+
return ComponentWithEffect(key=key)
602581

603582
@idom.component
604583
def ComponentWithEffect():
@@ -611,13 +590,14 @@ def bad_cleanup():
611590

612591
return idom.html.div()
613592

614-
with idom.Layout(OuterComponent()) as layout:
615-
await layout.render()
616-
outer_component_hook.latest.schedule_render()
617-
await layout.render() # no error
618-
619-
first_log_line = next(iter(caplog.records)).msg.split("\n", 1)[0]
620-
assert re.match("Pre-unmount effect .*? failed", first_log_line)
593+
with assert_idom_did_log(
594+
match_message=r"Pre-unmount effect .*? failed",
595+
error_type=ValueError,
596+
):
597+
with idom.Layout(OuterComponent()) as layout:
598+
await layout.render()
599+
set_key.current("second")
600+
await layout.render() # no error
621601

622602

623603
async def test_use_reducer():

0 commit comments

Comments
 (0)