Skip to content

Commit a08dbbb

Browse files
committed
get some tests working
1 parent 6ee621e commit a08dbbb

File tree

2 files changed

+130
-56
lines changed

2 files changed

+130
-56
lines changed

src/idom/testing.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,27 @@
22

33
import asyncio
44
import logging
5+
import operator
56
import re
67
import shutil
8+
import time
79
from contextlib import AsyncExitStack, contextmanager
8-
from functools import wraps
10+
from functools import partial, wraps
11+
from inspect import isawaitable, iscoroutinefunction
912
from traceback import format_exception
1013
from types import TracebackType
1114
from typing import (
1215
Any,
16+
Awaitable,
1317
Callable,
18+
Coroutine,
1419
Dict,
20+
Generic,
1521
Iterator,
1622
List,
1723
NoReturn,
1824
Optional,
25+
Sequence,
1926
Tuple,
2027
Type,
2128
TypeVar,
@@ -26,6 +33,7 @@
2633
from weakref import ref
2734

2835
from playwright.async_api import Browser, BrowserContext, Page, async_playwright
36+
from typing_extensions import ParamSpec
2937

3038
from idom import html
3139
from idom.config import IDOM_WEB_MODULES_DIR
@@ -49,6 +57,59 @@
4957
_Self = TypeVar("_Self")
5058

5159

60+
def assert_same_items(left: Sequence[Any], right: Sequence[Any]) -> None:
61+
"""Check that two unordered sequences are equal (only works if reprs are equal)"""
62+
sorted_left = list(sorted(left, key=repr))
63+
sorted_right = list(sorted(right, key=repr))
64+
assert sorted_left == sorted_right
65+
66+
67+
_P = ParamSpec("_P")
68+
_R = TypeVar("_R")
69+
_DEFAULT_TIMEOUT = 3.0
70+
71+
72+
class poll(Generic[_R]):
73+
"""Wait until the result of an sync or async function meets some condition"""
74+
75+
def __init__(
76+
self,
77+
function: Callable[_P, Awaitable[_R] | _R],
78+
*args: _P.args,
79+
**kwargs: _P.kwargs,
80+
) -> None:
81+
if iscoroutinefunction(function):
82+
83+
async def until(
84+
condition: Callable[[_R], bool], timeout: float = _DEFAULT_TIMEOUT
85+
) -> _R:
86+
started_at = time.time()
87+
while not condition(await function(*args, **kwargs)):
88+
if (time.time() - started_at) > timeout:
89+
raise TimeoutError()
90+
91+
else:
92+
93+
def until(
94+
condition: Callable[[_R], bool] | Any, timeout: float = _DEFAULT_TIMEOUT
95+
) -> _R:
96+
started_at = time.time()
97+
while not condition(function(*args, **kwargs)):
98+
if (time.time() - started_at) > timeout:
99+
raise TimeoutError()
100+
101+
self.until: Callable[[Callable[[_R], bool]], Any] = until
102+
"""Check that the coroutines result meets a condition within the timeout"""
103+
104+
def eq(self, right: Any, timeout: float = _DEFAULT_TIMEOUT) -> Any:
105+
"""Wait until the result is equal to the given value"""
106+
return self.until(lambda left: left == right, timeout)
107+
108+
def ne(self, right: Any, timeout: float = _DEFAULT_TIMEOUT) -> Any:
109+
"""Wait until the result is not equal to the given value"""
110+
return self.until(lambda left: left != right, timeout)
111+
112+
52113
class DisplayFixture:
53114
"""A fixture for running web-based tests using ``playwright``"""
54115

tests/test_widgets.py

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1+
import asyncio
12
from base64 import b64encode
23
from pathlib import Path
34

4-
from selenium.webdriver.common.keys import Keys
5-
65
import idom
7-
from tests.tooling.browser import send_keys
6+
from idom.testing import DisplayFixture, poll
87

98

109
HERE = Path(__file__).parent
@@ -14,7 +13,7 @@ def test_multiview_repr():
1413
assert str(idom.widgets.MultiViewMount({})) == "MultiViewMount({})"
1514

1615

17-
def test_hostwap_update_on_change(driver, display):
16+
async def test_hostwap_update_on_change(display: DisplayFixture):
1817
"""Ensure shared hotswapping works
1918
2019
This basically means that previously rendered views of a hotswap component get updated
@@ -48,15 +47,15 @@ async def on_click(event):
4847

4948
return idom.html.div(incr, hotswap_view)
5049

51-
display(ButtonSwapsDivs)
50+
page = await display.show(ButtonSwapsDivs)
5251

53-
client_incr_button = driver.find_element("id", "incr-button")
52+
client_incr_button = await page.wait_for_selector("#incr-button")
5453

55-
driver.find_element("id", "hotswap-1")
56-
client_incr_button.click()
57-
driver.find_element("id", "hotswap-2")
58-
client_incr_button.click()
59-
driver.find_element("id", "hotswap-3")
54+
await page.wait_for_selector("#hotswap-1")
55+
await client_incr_button.click()
56+
await page.wait_for_selector("#hotswap-2")
57+
await client_incr_button.click()
58+
await page.wait_for_selector("#hotswap-3")
6059

6160

6261
IMAGE_SRC_BYTES = b"""
@@ -67,43 +66,48 @@ async def on_click(event):
6766
BASE64_IMAGE_SRC = b64encode(IMAGE_SRC_BYTES).decode()
6867

6968

70-
def test_image_from_string(driver, display):
69+
async def test_image_from_string(display: DisplayFixture):
7170
src = IMAGE_SRC_BYTES.decode()
72-
display(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"}))
73-
client_img = driver.find_element("id", "a-circle-1")
74-
assert BASE64_IMAGE_SRC in client_img.get_attribute("src")
71+
page = await display.show(
72+
lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"})
73+
)
74+
client_img = await page.wait_for_selector("#a-circle-1")
75+
assert BASE64_IMAGE_SRC in (await client_img.get_attribute("src"))
7576

7677

77-
def test_image_from_bytes(driver, display):
78+
async def test_image_from_bytes(display: DisplayFixture):
7879
src = IMAGE_SRC_BYTES
79-
display(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"}))
80-
client_img = driver.find_element("id", "a-circle-1")
81-
assert BASE64_IMAGE_SRC in client_img.get_attribute("src")
80+
page = await display.show(
81+
lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"})
82+
)
83+
client_img = await page.wait_for_selector("#a-circle-1")
84+
assert BASE64_IMAGE_SRC in (await client_img.get_attribute("src"))
8285

8386

84-
def test_use_linked_inputs(driver, driver_wait, display):
87+
async def test_use_linked_inputs(display: DisplayFixture):
8588
@idom.component
8689
def SomeComponent():
8790
i_1, i_2 = idom.widgets.use_linked_inputs([{"id": "i_1"}, {"id": "i_2"}])
8891
return idom.html.div(i_1, i_2)
8992

90-
display(SomeComponent)
93+
page = await display.show(SomeComponent)
9194

92-
input_1 = driver.find_element("id", "i_1")
93-
input_2 = driver.find_element("id", "i_2")
95+
input_1 = await page.wait_for_selector("#i_1")
96+
input_2 = await page.wait_for_selector("#i_2")
9497

95-
send_keys(input_1, "hello")
98+
await input_1.type("hello", delay=20)
9699

97-
driver_wait.until(lambda d: input_1.get_attribute("value") == "hello")
98-
driver_wait.until(lambda d: input_2.get_attribute("value") == "hello")
100+
assert (await input_1.evaluate("e => e.value")) == "hello"
101+
assert (await input_2.evaluate("e => e.value")) == "hello"
99102

100-
send_keys(input_2, " world")
103+
await input_2.focus()
104+
await input_2.type(" world", delay=20)
101105

102-
driver_wait.until(lambda d: input_1.get_attribute("value") == "hello world")
103-
driver_wait.until(lambda d: input_2.get_attribute("value") == "hello world")
106+
assert (await input_1.evaluate("e => e.value")) == "hello world"
107+
assert (await input_2.evaluate("e => e.value")) == "hello world"
104108

105109

106-
def test_use_linked_inputs_on_change(driver, driver_wait, display):
110+
async def test_use_linked_inputs_on_change(display: DisplayFixture):
107111
value = idom.Ref(None)
108112

109113
@idom.component
@@ -114,21 +118,24 @@ def SomeComponent():
114118
)
115119
return idom.html.div(i_1, i_2)
116120

117-
display(SomeComponent)
121+
page = await display.show(SomeComponent)
122+
123+
input_1 = await page.wait_for_selector("#i_1")
124+
input_2 = await page.wait_for_selector("#i_2")
118125

119-
input_1 = driver.find_element("id", "i_1")
120-
input_2 = driver.find_element("id", "i_2")
126+
await input_1.type("hello", delay=20)
121127

122-
send_keys(input_1, "hello")
128+
poll_value = poll(lambda: value.current)
123129

124-
driver_wait.until(lambda d: value.current == "hello")
130+
poll_value.eq("hello")
125131

126-
send_keys(input_2, " world")
132+
await input_2.focus()
133+
await input_2.type(" world", delay=20)
127134

128-
driver_wait.until(lambda d: value.current == "hello world")
135+
poll_value.eq("hello world")
129136

130137

131-
def test_use_linked_inputs_on_change_with_cast(driver, driver_wait, display):
138+
async def test_use_linked_inputs_on_change_with_cast(display: DisplayFixture):
132139
value = idom.Ref(None)
133140

134141
@idom.component
@@ -138,21 +145,24 @@ def SomeComponent():
138145
)
139146
return idom.html.div(i_1, i_2)
140147

141-
display(SomeComponent)
148+
page = await display.show(SomeComponent)
142149

143-
input_1 = driver.find_element("id", "i_1")
144-
input_2 = driver.find_element("id", "i_2")
150+
input_1 = await page.wait_for_selector("#i_1")
151+
input_2 = await page.wait_for_selector("#i_2")
145152

146-
send_keys(input_1, "1")
153+
await input_1.type("1")
147154

148-
driver_wait.until(lambda d: value.current == 1)
155+
poll_value = poll(lambda: value.current)
149156

150-
send_keys(input_2, "2")
157+
poll_value.eq(1)
151158

152-
driver_wait.until(lambda d: value.current == 12)
159+
await input_2.focus()
160+
await input_2.type("2")
153161

162+
poll_value.eq(12)
154163

155-
def test_use_linked_inputs_ignore_empty(driver, driver_wait, display):
164+
165+
async def test_use_linked_inputs_ignore_empty(display: DisplayFixture):
156166
value = idom.Ref(None)
157167

158168
@idom.component
@@ -164,19 +174,22 @@ def SomeComponent():
164174
)
165175
return idom.html.div(i_1, i_2)
166176

167-
display(SomeComponent)
177+
page = await display.show(SomeComponent)
178+
179+
input_1 = await page.wait_for_selector("#i_1")
180+
input_2 = await page.wait_for_selector("#i_2")
168181

169-
input_1 = driver.find_element("id", "i_1")
170-
input_2 = driver.find_element("id", "i_2")
182+
await input_1.type("1")
171183

172-
send_keys(input_1, "1")
184+
poll_value = poll(lambda: value.current)
173185

174-
driver_wait.until(lambda d: value.current == "1")
186+
poll_value.eq("1")
175187

176-
send_keys(input_2, Keys.BACKSPACE)
188+
await input_2.focus()
189+
await input_2.press("Backspace")
177190

178-
assert value.current == "1"
191+
poll_value.eq("1")
179192

180-
send_keys(input_1, "2")
193+
await input_1.type("2")
181194

182-
driver_wait.until(lambda d: value.current == "2")
195+
poll_value.eq("2")

0 commit comments

Comments
 (0)