From 54af7c5bbe890d9a66aefe121bdb5428f3d50c03 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Mon, 19 Jul 2021 22:36:54 -0700 Subject: [PATCH 1/4] add basic test of IDOM --- noxfile.py | 8 +++-- requirements/test-env.txt | 5 +++ src/django_idom/__init__.py | 1 + src/django_idom/websocket_consumer.py | 33 ++++++++++-------- tests/test_app/asgi.py | 6 ++-- .../test_app/management/commands/build_js.py | 2 ++ tests/test_app/settings.py | 8 +++-- tests/test_app/tests.py | 34 +++++++++++++++++-- tests/test_app/urls.py | 2 ++ tests/test_app/views.py | 22 +++++++++++- 10 files changed, 96 insertions(+), 25 deletions(-) diff --git a/noxfile.py b/noxfile.py index 3a7b9d81..ced51466 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,11 +46,13 @@ def test(session: Session) -> None: @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.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/__init__.py b/src/django_idom/__init__.py index b62571ae..ccb3667f 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -2,4 +2,5 @@ from .websocket_consumer import IdomAsyncWebSocketConsumer + __all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py index c29330b5..2fae2869 100644 --- a/src/django_idom/websocket_consumer.py +++ b/src/django_idom/websocket_consumer.py @@ -3,10 +3,9 @@ from typing import Any from channels.generic.websocket import AsyncJsonWebsocketConsumer - -from idom.core.layout import Layout from idom.core.dispatcher import dispatch_single_view -from idom.core.component import ComponentConstructor +from idom.core.layout import Layout, LayoutEvent +from idom.core.proto import ComponentConstructor class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer): @@ -20,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/test_app/asgi.py b/tests/test_app/asgi.py index 0db87cf4..56726f27 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -12,7 +12,8 @@ from django.conf.urls import url from django.core.asgi import get_asgi_application -from .views import HelloWorld +from .views import Root + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") @@ -23,11 +24,12 @@ from django_idom import IdomAsyncWebSocketConsumer + application = ProtocolTypeRouter( { "http": http_asgi_app, "websocket": URLRouter( - [url("", IdomAsyncWebSocketConsumer.as_asgi(component=HelloWorld))] + [url("", IdomAsyncWebSocketConsumer.as_asgi(component=Root))] ), } ) diff --git a/tests/test_app/management/commands/build_js.py b/tests/test_app/management/commands/build_js.py index cb4fc433..61af5ed9 100644 --- a/tests/test_app/management/commands/build_js.py +++ b/tests/test_app/management/commands/build_js.py @@ -1,7 +1,9 @@ import subprocess from pathlib import Path + from django.core.management.base import BaseCommand + HERE = Path(__file__).parent JS_DIR = HERE.parent.parent.parent / "js" diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 3eb5e546..67f93f40 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -12,6 +12,7 @@ import os from pathlib import Path + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -78,8 +79,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"), + }, + }, } diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index 7ce503c2..485349f0 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,3 +1,33 @@ -from django.test import TestCase +from channels.testing import ChannelsLiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait -# Create your tests here. + +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): + driver = webdriver.Chrome() + driver.set_page_load_timeout(page_load_timeout) + driver.implicitly_wait(implicit_wait_timeout) + return driver diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py index 91dab28d..4449e447 100644 --- a/tests/test_app/urls.py +++ b/tests/test_app/urls.py @@ -18,6 +18,8 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.urls import path + from .views import base_template + urlpatterns = [path("", base_template)] diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 50a4385b..a996eb1e 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -1,6 +1,6 @@ import idom -from django.template import loader from django.http import HttpResponse +from django.template import loader def base_template(request): @@ -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}", + ), + ) From 2091408a3740c1ea697653fe1b061fcc50c232c8 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Tue, 20 Jul 2021 00:09:22 -0700 Subject: [PATCH 2/4] fix rollup warning for this --- tests/js/rollup.config.js | 11 +++++++++++ 1 file changed, 11 insertions(+) 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); + }, }; From c377f3d63aa8060311935ff3fac87ebfce47ca78 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Tue, 20 Jul 2021 00:13:37 -0700 Subject: [PATCH 3/4] fix style issues --- src/django_idom/__init__.py | 1 + tests/test_app/asgi.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 79adf834..60932c7a 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,4 +1,5 @@ from .websocket_consumer import IdomAsyncWebSocketConsumer + __version__ = "0.0.1" __all__ = ["IdomAsyncWebSocketConsumer"] diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py index 906b0542..113b147f 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -12,10 +12,10 @@ from django.conf.urls import url from django.core.asgi import get_asgi_application -from .views import Root - from django_idom import IdomAsyncWebSocketConsumer # noqa: E402 +from .views import Root + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") @@ -25,7 +25,6 @@ from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 - application = ProtocolTypeRouter( { "http": http_asgi_app, From 4e1a970281c0c6b54fc0288948fd7ee7c87444fa Mon Sep 17 00:00:00 2001 From: rmorshea Date: Tue, 20 Jul 2021 00:27:29 -0700 Subject: [PATCH 4/4] allow tests to run in headless mode in CI --- .github/workflows/test.yml | 2 +- noxfile.py | 3 ++- tests/test_app/tests.py | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) 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 ced51466..4a943345 100644 --- a/noxfile.py +++ b/noxfile.py @@ -39,7 +39,7 @@ 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") @@ -51,6 +51,7 @@ def test_suite(session: Session) -> None: 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") diff --git a/tests/test_app/tests.py b/tests/test_app/tests.py index 485349f0..1a80ee5a 100644 --- a/tests/test_app/tests.py +++ b/tests/test_app/tests.py @@ -1,3 +1,5 @@ +import os + from channels.testing import ChannelsLiveServerTestCase from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait @@ -27,7 +29,9 @@ def test_counter(self): def make_driver(page_load_timeout, implicit_wait_timeout): - driver = webdriver.Chrome() + 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