Skip to content

Commit f41bda5

Browse files
author
User
committed
Fix flaky test_form_sync_events by retrying the empty-form submit
`test_form_sync_events` intermittently times out waiting for `#error[data-value='true']` after submitting the empty form. The `DjangoForm` client component registers a native `submit` listener that calls `preventDefault()` and forwards the form data over the WebSocket, but the listener is attached in `componentDidMount` and can momentarily lag the rendered `<form>` under load. A click landing during that window performs a native form submission: the browser reloads the page (a second `Starting client...` with no preceding `Disconnected!`, matching the CI signature), which resets all ReactPy state so `on_error` never fires. The existing retry guard only covered the later *filled* re-submit. Extend it into a shared `_retry_submit_until(selector, fill=...)` helper and use it for the empty-submit path too (and, symmetrically, for the async-event form which shares the same interaction). Re-clicking the submit until the awaited callback surfaces makes the interaction deterministic instead of racing the mount. Test-only change; no product code is touched.
1 parent 0b4f2d5 commit f41bda5

1 file changed

Lines changed: 34 additions & 21 deletions

File tree

‎tests/test_app/tests/test_components.py‎

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# type: ignore
22
# ruff: noqa: RUF012
3+
from __future__ import annotations
4+
35
import os
46
import socket
57
from uuid import uuid4
@@ -910,19 +912,22 @@ def test_form_orm_model(self):
910912
finally:
911913
os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE")
912914

913-
def _retry_until_true(self, selector: str) -> None:
914-
"""Poll for `#success[data-value='true']` after submitting the filled form.
915-
916-
Filling the `char_field` triggers the form's `onChange` handler, which causes an
917-
async server re-render that reconciles (and can transiently remount) the submit
918-
button. A single Playwright click on `input[type=submit]` may therefore land on
919-
the form element instead of the button, swallowing the `submit` event and losing
920-
the valid submission. This helper re-fills the field and re-clicks submit until
921-
the `on_success` callback actually takes effect, making the interaction
922-
deterministic instead of racing the reconciliation.
915+
def _retry_submit_until(self, selector: str, *, fill: str | None) -> None:
916+
"""Re-submit the form until an awaited state ``selector`` becomes visible.
917+
918+
The ``DjangoForm`` client component registers a native ``submit`` listener that
919+
calls ``preventDefault()`` and forwards the form data over the WebSocket. That
920+
listener is attached in ``componentDidMount`` and can momentarily lag the
921+
rendered ``<form>`` (and the button can transiently re-mount after an
922+
``onChange``-triggered async re-render). A click landing during either window
923+
performs a *native* form submission: the browser reloads the page, which resets
924+
all ReactPy state, so the awaited callback never appears. Retry the submit
925+
(re-typing ``char_field`` when ``fill`` is set) until ``selector`` is satisfied
926+
instead of racing the mount/reconcile. Pass ``fill=None`` to submit unchanged.
923927
"""
924928
for _ in range(3):
925-
self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
929+
if fill is not None:
930+
self.page.wait_for_selector("#id_char_field").type(fill, delay=DELAY)
926931
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
927932
try:
928933
self.page.wait_for_selector(selector, timeout=5000)
@@ -942,19 +947,23 @@ def test_form_sync_events(self):
942947
self.page.wait_for_selector("#receive_data[data-value='false']")
943948
self.page.wait_for_selector("#change[data-value='false']")
944949

945-
# Submit empty the form
946-
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
950+
# Submit the empty form and wait for the `on_error` callback to take effect. The
951+
# client-side `preventDefault` submit interceptor can momentarily lag the rendered
952+
# <form> under load, so a click that lands first performs a native submit that
953+
# reloads the page and resets state. Re-click the (still-empty) submit until the
954+
# error surfaces rather than racing the mount.
955+
self._retry_submit_until("#error[data-value='true']", fill=None)
947956

948-
# The empty form was submitted, should result in an error
957+
# Empty submit is invalid: `on_error` and `on_receive_data` fired, but nothing was
958+
# typed so `on_change` never fired and nothing succeeded.
949959
self.page.wait_for_selector("#success[data-value='false']")
950-
self.page.wait_for_selector("#error[data-value='true']")
951960
self.page.wait_for_selector("#receive_data[data-value='true']")
952961
self.page.wait_for_selector("#change[data-value='false']")
953962

954963
# Fill out the form and re-submit. The `onChange`-triggered async re-render can
955964
# transiently remount the submit button, so retry (re-filling + re-clicking) until
956965
# the `on_success` callback visibly takes effect rather than racing the reconcile.
957-
self._retry_until_true("#success[data-value='true']")
966+
self._retry_submit_until("#success[data-value='true']", fill="test")
958967

959968
# Form should have been successfully submitted
960969
self.page.wait_for_selector("#error[data-value='true']")
@@ -971,19 +980,23 @@ def test_form_async_events(self):
971980
self.page.wait_for_selector("#receive_data[data-value='false']")
972981
self.page.wait_for_selector("#change[data-value='false']")
973982

974-
# Submit empty the form
975-
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
983+
# Submit the empty form and wait for the `on_error` callback to take effect. The
984+
# client-side `preventDefault` submit interceptor can momentarily lag the rendered
985+
# <form> under load, so a click that lands first performs a native submit that
986+
# reloads the page and resets state. Re-click the (still-empty) submit until the
987+
# error surfaces rather than racing the mount.
988+
self._retry_submit_until("#error[data-value='true']", fill=None)
976989

977-
# The empty form was submitted, should result in an error
990+
# Empty submit is invalid: `on_error` and `on_receive_data` fired, but nothing was
991+
# typed so `on_change` never fired and nothing succeeded.
978992
self.page.wait_for_selector("#success[data-value='false']")
979-
self.page.wait_for_selector("#error[data-value='true']")
980993
self.page.wait_for_selector("#receive_data[data-value='true']")
981994
self.page.wait_for_selector("#change[data-value='false']")
982995

983996
# Fill out the form and re-submit. The `onChange`-triggered async re-render can
984997
# transiently remount the submit button, so retry (re-filling + re-clicking) until
985998
# the `on_success` callback visibly takes effect rather than racing the reconcile.
986-
self._retry_until_true("#success[data-value='true']")
999+
self._retry_submit_until("#success[data-value='true']", fill="test")
9871000

9881001
# Form should have been successfully submitted
9891002
self.page.wait_for_selector("#error[data-value='true']")

0 commit comments

Comments
 (0)