diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5edd2881..d3097b33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,4 +32,4 @@ jobs: run: | npm install -g npm@latest npm --version - nox -s test + nox -s test -- --headless diff --git a/noxfile.py b/noxfile.py index 3a7b9d81..4a943345 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,18 +39,21 @@ def format(session: Session) -> None: def test(session: Session) -> None: """Run the complete test suite""" session.install("--upgrade", "pip", "setuptools", "wheel") - session.notify("test_suite") + session.notify("test_suite", posargs=session.posargs) session.notify("test_style") @nox.session def test_suite(session: Session) -> None: """Run the Python-based test suite""" - session.env["IDOM_DEBUG_MODE"] = "1" install_requirements_file(session, "test-env") session.install(".[all]") - session.chdir("tests") - session.run("figure-it-out") + + session.chdir(HERE / "tests") + session.env["IDOM_DEBUG_MODE"] = "1" + session.env["SELENIUM_HEADLESS"] = str(int("--headless" in session.posargs)) + session.run("python", "manage.py", "build_js") + session.run("python", "manage.py", "test") @nox.session diff --git a/requirements/test-env.txt b/requirements/test-env.txt index d3e4ba56..c100c316 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1 +1,6 @@ django +selenium + +# required due issue with channels: +# https://github.com/django/channels/issues/1639#issuecomment-817994671 +twisted<21 diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py index a9fb13d4..2fae2869 100644 --- a/src/django_idom/websocket_consumer.py +++ b/src/django_idom/websocket_consumer.py @@ -3,9 +3,9 @@ from typing import Any from channels.generic.websocket import AsyncJsonWebsocketConsumer -from idom.core.component import ComponentConstructor from idom.core.dispatcher import dispatch_single_view -from idom.core.layout import Layout +from idom.core.layout import Layout, LayoutEvent +from idom.core.proto import ComponentConstructor class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer): @@ -19,22 +19,26 @@ def __init__( async def connect(self) -> None: await super().connect() - self._idom_recv_queue = recv_queue = asyncio.Queue() - self._idom_dispatcher_future = asyncio.ensure_future( - dispatch_single_view( - Layout(self._idom_component_constructor()), - self.send_json, - recv_queue.get, - ) - ) + self._idom_dispatcher_future = asyncio.ensure_future(self._run_dispatch_loop()) - async def close(self, *args: Any, **kwargs: Any) -> None: + async def disconnect(self, code: int) -> None: if self._idom_dispatcher_future.done(): await self._idom_dispatcher_future else: self._idom_dispatcher_future.cancel() - await asyncio.wait([self._idom_dispatcher_future]) - await super().close(*args, **kwargs) + await super().disconnect(code) async def receive_json(self, content: Any, **kwargs: Any) -> None: - await self._idom_recv_queue.put(content) + await self._idom_recv_queue.put(LayoutEvent(**content)) + + async def _run_dispatch_loop(self): + self._idom_recv_queue = recv_queue = asyncio.Queue() + try: + await dispatch_single_view( + Layout(self._idom_component_constructor()), + self.send_json, + recv_queue.get, + ) + except Exception: + await self.close() + raise diff --git a/tests/js/rollup.config.js b/tests/js/rollup.config.js index da897f27..ad597a61 100644 --- a/tests/js/rollup.config.js +++ b/tests/js/rollup.config.js @@ -19,4 +19,15 @@ export default { ), }), ], + onwarn: function (warning) { + // Skip certain warnings + + // should intercept ... but doesn't in some rollup versions + if (warning.code === "THIS_IS_UNDEFINED") { + return; + } + + // console.warn everything else + console.warn(warning.message); + }, }; diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py index 3f868302..113b147f 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -14,7 +14,7 @@ from django_idom import IdomAsyncWebSocketConsumer # noqa: E402 -from .views import HelloWorld +from .views import Root os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") @@ -29,7 +29,7 @@ { "http": http_asgi_app, "websocket": URLRouter( - [url("", IdomAsyncWebSocketConsumer.as_asgi(component=HelloWorld))] + [url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))] ), } ) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index d6d3414b..73eda0fa 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -72,8 +72,11 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "TEST": { + "NAME": os.path.join(BASE_DIR, "db_test.sqlite3"), + }, + }, } # Password validation diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index 1a839f8d..1a80ee5a 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,5 +1,37 @@ -from django.test import TestCase +import os +from channels.testing import ChannelsLiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait -class Temp(TestCase): - pass + +class TestIdomCapabilities(ChannelsLiveServerTestCase): + def setUp(self): + self.driver = make_driver(5, 5) + self.driver.get(self.live_server_url) + + def tearDown(self) -> None: + self.driver.quit() + + def wait_until(self, condition, timeout=5): + WebDriverWait(self.driver, timeout).until(lambda driver: condition()) + + def test_hello_world(self): + self.driver.find_element_by_id("hello-world") + + def test_counter(self): + button = self.driver.find_element_by_id("counter-inc") + count = self.driver.find_element_by_id("counter-num") + + for i in range(5): + self.wait_until(lambda: count.get_attribute("data-count") == str(i)) + button.click() + + +def make_driver(page_load_timeout, implicit_wait_timeout): + options = webdriver.ChromeOptions() + options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0))) + driver = webdriver.Chrome(options=options) + driver.set_page_load_timeout(page_load_timeout) + driver.implicitly_wait(implicit_wait_timeout) + return driver diff --git a/tests/test_app/views.py b/tests/test_app/views.py index ccc6560e..a996eb1e 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -9,6 +9,26 @@ def base_template(request): return HttpResponse(template.render(context, request)) +@idom.component +def Root(): + return idom.html.div(HelloWorld(), Counter()) + + @idom.component def HelloWorld(): return idom.html.h1({"id": "hello-world"}, "Hello World!") + + +@idom.component +def Counter(): + count, set_count = idom.hooks.use_state(0) + return idom.html.div( + idom.html.button( + {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, + "Click me!", + ), + idom.html.p( + {"id": "counter-num", "data-count": count}, + f"Current count is: {count}", + ), + )