Skip to content

Commit d9416d9

Browse files
committed
Add tests
1 parent d561c88 commit d9416d9

File tree

7 files changed

+239
-7
lines changed

7 files changed

+239
-7
lines changed

tests/test_app/forms/urls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
urlpatterns = [
66
path("form/", views.form),
77
path("form/bootstrap/", views.bootstrap_form),
8-
path("form/database/", views.databased_backed_form),
8+
path("form/model/", views.model_form),
99
]

tests/test_app/forms/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ def bootstrap_form(request):
99
return render(request, "bootstrap_form.html", {})
1010

1111

12-
def databased_backed_form(request):
13-
return render(request, "database_backed_form.html", {})
12+
def model_form(request):
13+
return render(request, "model_form.html", {})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{% extends "admin/login.html" %}
2+
3+
{% block content %}
4+
{{ block.super }}
5+
<script>document.getElementById("id_username").value = "admin"; document.getElementById("id_password").value = "password";</script>
6+
{% endblock %}

tests/test_app/templates/bootstrap_form.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% load static %} {% load reactpy %}
1+
{% load static %} {% load reactpy %} {% load django_bootstrap5 %}
22
<!DOCTYPE html>
33
<html lang="en">
44

tests/test_app/templates/database_backed_form.html renamed to tests/test_app/templates/model_form.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</head>
1818

1919
<body>
20-
<h1>ReactPy Database Form Test Page</h1>
20+
<h1>ReactPy Model Form Test Page</h1>
2121
<hr>
2222
{% component "test_app.forms.components.database_backed_form" %}
2323
<hr>

tests/test_app/tests/test_components.py

+222-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from time import sleep
55

66
import pytest
7-
from playwright.sync_api import TimeoutError
7+
from playwright.sync_api import TimeoutError, expect
88

99
from reactpy_django.models import ComponentSession
1010
from reactpy_django.utils import strtobool
1111

12-
from .utils import GITHUB_ACTIONS, PlaywrightTestCase
12+
from .utils import GITHUB_ACTIONS, PlaywrightTestCase, navigate_to_page
1313

1414
CLICK_DELAY = 250 if strtobool(GITHUB_ACTIONS) else 25 # Delay in miliseconds.
1515

@@ -579,3 +579,223 @@ def test_offline_components(self):
579579
self._server_process.join()
580580
self.page.wait_for_selector("div:not([hidden]) > #offline")
581581
assert self.page.query_selector("div[hidden] > #online") is not None
582+
583+
584+
class FormTests(PlaywrightTestCase):
585+
def test_basic_form(self):
586+
navigate_to_page(self, "/form/")
587+
588+
try:
589+
from test_app.models import TodoItem
590+
591+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
592+
593+
if TodoItem.objects.count() == 0:
594+
TodoItem(done=False, text="First").save()
595+
TodoItem(done=True, text="Second").save()
596+
TodoItem(done=False, text="Third").save()
597+
finally:
598+
os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE")
599+
600+
self.page.wait_for_selector("form")
601+
self.page.wait_for_selector("#id_boolean_field")
602+
self.page.wait_for_selector("#id_char_field")
603+
self.page.wait_for_selector("#id_choice_field")
604+
self.page.wait_for_selector("#id_date_field")
605+
self.page.wait_for_selector("#id_date_time_field")
606+
self.page.wait_for_selector("#id_decimal_field")
607+
self.page.wait_for_selector("#id_duration_field")
608+
self.page.wait_for_selector("#id_email_field")
609+
self.page.wait_for_selector("#id_file_path_field")
610+
self.page.wait_for_selector("#id_float_field")
611+
self.page.wait_for_selector("#id_generic_ip_address_field")
612+
self.page.wait_for_selector("#id_integer_field")
613+
self.page.wait_for_selector("#id_float_field")
614+
self.page.wait_for_selector("#id_json_field")
615+
self.page.wait_for_selector("#id_multiple_choice_field")
616+
self.page.wait_for_selector("#id_null_boolean_field")
617+
self.page.wait_for_selector("#id_regex_field")
618+
self.page.wait_for_selector("#id_slug_field")
619+
self.page.wait_for_selector("#id_time_field")
620+
self.page.wait_for_selector("#id_typed_choice_field")
621+
self.page.wait_for_selector("#id_typed_multiple_choice_field")
622+
self.page.wait_for_selector("#id_url_field")
623+
self.page.wait_for_selector("#id_uuid_field")
624+
self.page.wait_for_selector("#id_combo_field")
625+
self.page.wait_for_selector("#id_password_field")
626+
self.page.wait_for_selector("#id_model_choice_field")
627+
self.page.wait_for_selector("#id_model_multiple_choice_field")
628+
629+
sleep(1)
630+
self.page.wait_for_selector("input[type=submit]").click(delay=CLICK_DELAY)
631+
self.page.wait_for_selector(".errorlist")
632+
633+
# Submitting an empty form should result in 22 error elements.
634+
# The number of errors may change if/when new test form elements are created.
635+
assert len(self.page.query_selector_all(".errorlist")) == 22
636+
637+
# Fill out the form
638+
self.page.wait_for_selector("#id_boolean_field").click(delay=CLICK_DELAY)
639+
expect(self.page.locator("#id_boolean_field")).to_be_checked()
640+
641+
self.page.locator("#id_char_field").type("test", delay=CLICK_DELAY)
642+
self.page.locator("#id_choice_field").select_option("2")
643+
self.page.locator("#id_date_field").type("2021-01-01", delay=CLICK_DELAY)
644+
self.page.locator("#id_date_time_field").type("2021-01-01 01:01:00", delay=CLICK_DELAY)
645+
self.page.locator("#id_decimal_field").type("0.123", delay=CLICK_DELAY)
646+
self.page.locator("#id_duration_field").type("1", delay=CLICK_DELAY)
647+
self.page.locator("#id_email_field").type("[email protected]", delay=CLICK_DELAY)
648+
self.page.locator("#id_file_path_field").select_option("manage.py")
649+
self.page.locator("#id_float_field").type("1.2345", delay=CLICK_DELAY)
650+
self.page.locator("#id_generic_ip_address_field").type("127.0.0.1", delay=CLICK_DELAY)
651+
self.page.locator("#id_integer_field").type("123", delay=CLICK_DELAY)
652+
self.page.locator("#id_json_field").clear()
653+
self.page.locator("#id_json_field").type('{"key": "value"}', delay=CLICK_DELAY)
654+
self.page.locator("#id_multiple_choice_field").select_option(["2", "3"])
655+
self.page.locator("#id_null_boolean_field").select_option("false")
656+
self.page.locator("#id_regex_field").type("12", delay=CLICK_DELAY)
657+
self.page.locator("#id_slug_field").type("my-slug-text", delay=CLICK_DELAY)
658+
self.page.locator("#id_time_field").type("01:01:00", delay=CLICK_DELAY)
659+
self.page.locator("#id_typed_choice_field").select_option("2")
660+
self.page.locator("#id_typed_multiple_choice_field").select_option(["1", "2"])
661+
self.page.locator("#id_url_field").type("http://example.com", delay=CLICK_DELAY)
662+
self.page.locator("#id_uuid_field").type("550e8400-e29b-41d4-a716-446655440000", delay=CLICK_DELAY)
663+
self.page.locator("#id_combo_field").type("[email protected]", delay=CLICK_DELAY)
664+
self.page.locator("#id_password_field").type("password", delay=CLICK_DELAY)
665+
666+
model_choice_field_options = self.page.query_selector_all("#id_model_multiple_choice_field option")
667+
model_choice_field_values: list[str] = [option.get_attribute("value") for option in model_choice_field_options]
668+
self.page.locator("#id_model_choice_field").select_option(model_choice_field_values[0])
669+
self.page.locator("#id_model_multiple_choice_field").select_option([
670+
model_choice_field_values[1],
671+
model_choice_field_values[2],
672+
])
673+
674+
self.page.wait_for_selector("input[type=submit]").click(delay=CLICK_DELAY)
675+
676+
# Wait for one of the error messages to disappear (indicating that the form has been re-rendered)
677+
expect(self.page.locator(".errorlist").all()[0]).not_to_be_attached()
678+
# Make sure no errors remain
679+
assert len(self.page.query_selector_all(".errorlist")) == 0
680+
681+
def test_bootstrap_form(self):
682+
navigate_to_page(self, "/form/bootstrap/")
683+
684+
try:
685+
from test_app.models import TodoItem
686+
687+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
688+
689+
if TodoItem.objects.count() == 0:
690+
TodoItem(done=False, text="First").save()
691+
TodoItem(done=True, text="Second").save()
692+
TodoItem(done=False, text="Third").save()
693+
finally:
694+
os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE")
695+
696+
self.page.wait_for_selector("form")
697+
self.page.wait_for_selector("#id_boolean_field")
698+
self.page.wait_for_selector("#id_char_field")
699+
self.page.wait_for_selector("#id_choice_field")
700+
self.page.wait_for_selector("#id_date_field")
701+
self.page.wait_for_selector("#id_date_time_field")
702+
self.page.wait_for_selector("#id_decimal_field")
703+
self.page.wait_for_selector("#id_duration_field")
704+
self.page.wait_for_selector("#id_email_field")
705+
self.page.wait_for_selector("#id_file_path_field")
706+
self.page.wait_for_selector("#id_float_field")
707+
self.page.wait_for_selector("#id_generic_ip_address_field")
708+
self.page.wait_for_selector("#id_integer_field")
709+
self.page.wait_for_selector("#id_float_field")
710+
self.page.wait_for_selector("#id_json_field")
711+
self.page.wait_for_selector("#id_multiple_choice_field")
712+
self.page.wait_for_selector("#id_null_boolean_field")
713+
self.page.wait_for_selector("#id_regex_field")
714+
self.page.wait_for_selector("#id_slug_field")
715+
self.page.wait_for_selector("#id_time_field")
716+
self.page.wait_for_selector("#id_typed_choice_field")
717+
self.page.wait_for_selector("#id_typed_multiple_choice_field")
718+
self.page.wait_for_selector("#id_url_field")
719+
self.page.wait_for_selector("#id_uuid_field")
720+
self.page.wait_for_selector("#id_combo_field")
721+
self.page.wait_for_selector("#id_password_field")
722+
self.page.wait_for_selector("#id_model_choice_field")
723+
self.page.wait_for_selector("#id_model_multiple_choice_field")
724+
725+
sleep(1)
726+
self.page.wait_for_selector("button[type=submit]").click(delay=CLICK_DELAY)
727+
self.page.wait_for_selector(".invalid-feedback")
728+
729+
# Submitting an empty form should result in 22 error elements.
730+
# The number of errors may change if/when new test form elements are created.
731+
assert len(self.page.query_selector_all(".invalid-feedback")) == 22
732+
733+
# Fill out the form
734+
self.page.wait_for_selector("#id_boolean_field").click(delay=CLICK_DELAY)
735+
expect(self.page.locator("#id_boolean_field")).to_be_checked()
736+
737+
self.page.locator("#id_char_field").type("test", delay=CLICK_DELAY)
738+
self.page.locator("#id_choice_field").select_option("2")
739+
self.page.locator("#id_date_field").type("2021-01-01", delay=CLICK_DELAY)
740+
self.page.locator("#id_date_time_field").type("2021-01-01 01:01:00", delay=CLICK_DELAY)
741+
self.page.locator("#id_decimal_field").type("0.123", delay=CLICK_DELAY)
742+
self.page.locator("#id_duration_field").type("1", delay=CLICK_DELAY)
743+
self.page.locator("#id_email_field").type("[email protected]", delay=CLICK_DELAY)
744+
self.page.locator("#id_file_path_field").select_option("manage.py")
745+
self.page.locator("#id_float_field").type("1.2345", delay=CLICK_DELAY)
746+
self.page.locator("#id_generic_ip_address_field").type("127.0.0.1", delay=CLICK_DELAY)
747+
self.page.locator("#id_integer_field").type("123", delay=CLICK_DELAY)
748+
self.page.locator("#id_json_field").clear()
749+
self.page.locator("#id_json_field").type('{"key": "value"}', delay=CLICK_DELAY)
750+
self.page.locator("#id_multiple_choice_field").select_option(["2", "3"])
751+
self.page.locator("#id_null_boolean_field").select_option("false")
752+
self.page.locator("#id_regex_field").type("12", delay=CLICK_DELAY)
753+
self.page.locator("#id_slug_field").type("my-slug-text", delay=CLICK_DELAY)
754+
self.page.locator("#id_time_field").type("01:01:00", delay=CLICK_DELAY)
755+
self.page.locator("#id_typed_choice_field").select_option("2")
756+
self.page.locator("#id_typed_multiple_choice_field").select_option(["1", "2"])
757+
self.page.locator("#id_url_field").type("http://example.com", delay=CLICK_DELAY)
758+
self.page.locator("#id_uuid_field").type("550e8400-e29b-41d4-a716-446655440000", delay=CLICK_DELAY)
759+
self.page.locator("#id_combo_field").type("[email protected]", delay=CLICK_DELAY)
760+
self.page.locator("#id_password_field").type("password", delay=CLICK_DELAY)
761+
762+
model_choice_field_options = self.page.query_selector_all("#id_model_multiple_choice_field option")
763+
model_choice_field_values: list[str] = [option.get_attribute("value") for option in model_choice_field_options]
764+
self.page.locator("#id_model_choice_field").select_option(model_choice_field_values[0])
765+
self.page.locator("#id_model_multiple_choice_field").select_option([
766+
model_choice_field_values[1],
767+
model_choice_field_values[2],
768+
])
769+
770+
self.page.wait_for_selector("button[type=submit]").click(delay=CLICK_DELAY)
771+
772+
# Wait for one of the error messages to disappear (indicating that the form has been re-rendered)
773+
expect(self.page.locator(".invalid-feedback").all()[0]).not_to_be_attached()
774+
# Make sure no errors remain
775+
assert len(self.page.query_selector_all(".invalid-feedback")) == 0
776+
777+
def test_model_form(self):
778+
navigate_to_page(self, "/form/model/")
779+
780+
self.page.wait_for_selector("form")
781+
782+
sleep(1)
783+
self.page.wait_for_selector("input[type=submit]").click(delay=CLICK_DELAY)
784+
self.page.wait_for_selector(".errorlist")
785+
786+
# Submitting an empty form should result in 1 error element.
787+
assert len(self.page.query_selector_all(".errorlist")) == 1
788+
789+
# Fill out the form
790+
self.page.locator("#id_text").type("test", delay=CLICK_DELAY)
791+
792+
self.page.wait_for_selector("input[type=submit]").click(delay=CLICK_DELAY)
793+
794+
# Wait for the error message to disappear (indicating that the form has been re-rendered)
795+
expect(self.page.locator(".errorlist").all()[0]).not_to_be_attached()
796+
797+
# Make sure no errors remain
798+
assert len(self.page.query_selector_all(".errorlist")) == 0
799+
800+
# Make sure text field is empty
801+
assert self.page.locator("#id_text").get_attribute("value") == ""

tests/test_app/tests/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,9 @@ def _post_teardown(self):
8787
"""Handled manually in `tearDownClass` to prevent TransactionTestCase from doing
8888
database flushing. This is needed to prevent a `SynchronousOnlyOperation` from
8989
occurring due to a bug within `ChannelsLiveServerTestCase`."""
90+
91+
92+
def navigate_to_page(self: PlaywrightTestCase, path: str):
93+
"""Redirect the page's URL to the given link, if the page is not already there."""
94+
if self.page.url != path:
95+
self.page.goto(f"http://{self.host}:{self._port}/{path.lstrip('/')}")

0 commit comments

Comments
 (0)