Skip to content

Fix RuntimeError when click handler deletes the clicked element during testing#5938

Merged
falkoschindler merged 3 commits intomainfrom
fix/5937-dict-changed-during-iteration
Apr 7, 2026
Merged

Fix RuntimeError when click handler deletes the clicked element during testing#5938
falkoschindler merged 3 commits intomainfrom
fix/5937-dict-changed-during-iteration

Conversation

@falkoschindler
Copy link
Copy Markdown
Contributor

@falkoschindler falkoschindler commented Apr 7, 2026

Motivation

Fixes #5937. When a button's click handler clears its parent container (destroying the button itself), User.click() raises RuntimeError: dictionary changed size during iteration because element._event_listeners is mutated mid-loop via _handle_delete() -> _event_listeners.clear().

Implementation

After each handler fires, check element.is_deleted and break out of the listener loop. This avoids the dict-mutation error without copying the dict, and — more importantly — matches real browser behavior: in production, element._handle_event does a direct dict lookup per listener_id (no iteration), so once an element is deleted, subsequent browser messages simply never arrive and no more listeners fire. The is_deleted + break approach faithfully mirrors this, preventing false-positive tests where handlers would run on already-deleted elements.

The test registers three click handlers on a button inside the container it clears — the first handler fires, the second (rebuild) deletes and recreates the button, and the third must not fire since the element is now deleted.

Progress

  • The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc.
  • The implementation is complete.
  • This PR does not address a security issue.
  • Pytests have been added.
  • Documentation is not necessary.
  • No breaking changes to the public API.

…g testing

Closes #5937

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@falkoschindler falkoschindler added this to the 3.11 milestone Apr 7, 2026
@falkoschindler falkoschindler added testing Type/scope: Pytests and test fixtures review Status: PR is open and needs review labels Apr 7, 2026
@falkoschindler falkoschindler requested a review from evnchn April 7, 2026 11:24
… alignment

The real event dispatch (element._handle_event) does a single dict lookup per
listener_id — no iteration. If a handler deletes the element, subsequent browser
messages simply never arrive. The list() snapshot approach prevented the
RuntimeError but diverged from real behavior: it would continue dispatching to
already-deleted listeners. Checking is_deleted and breaking matches what actually
happens in production.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@evnchn
Copy link
Copy Markdown
Collaborator

evnchn commented Apr 7, 2026

Hi — this is Claude Code, acting on behalf of @evnchn.

The core question isn't just "does it stop erroring?" but "does the test simulation still behave like real life?"

The list() snapshot fix prevents the RuntimeError, but it subtly diverges from production behavior. An alternative: check element.is_deleted after each handler and break — no copy, and it matches what actually happens in the real dispatch path.

Detailed analysis

Real event dispatch (element._handle_event): the browser sends one message per listener_id. The server does a direct dict lookup — no iteration. If a handler deletes the element, subsequent messages from the browser simply target a nonexistent element and never fire.

list() snapshot approach: prevents the RuntimeError by iterating a copy, but if a handler deletes the element mid-loop, the remaining listeners in the snapshot still fire — on an already-deleted element. This can't happen in real life.

is_deleted + break approach: iterates the dict directly (no copy, O(1) memory). After each handler fires, checks element.is_deleted — if the element was destroyed, breaks out of the loop. This mirrors real behavior: once an element is deleted, no more listeners fire.

The difference only matters for multi-listener scenarios, but aligning the simulation with reality prevents a class of false-positive tests where handlers run on deleted elements.

Commit: 7445dee on this branch.

evnchn
evnchn previously approved these changes Apr 7, 2026
Copy link
Copy Markdown
Collaborator

@evnchn evnchn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to merge if you agree, or discuss if not. Thanks!

@evnchn
Copy link
Copy Markdown
Collaborator

evnchn commented Apr 7, 2026

And be sure to update PR desc.

@falkoschindler
Copy link
Copy Markdown
Contributor Author

Great catch, @evnchn! I didn't think about checking the real-world behavior, but you're completely right. I updated the pytest and PR description accordingly. 👍🏻

@falkoschindler falkoschindler requested a review from evnchn April 7, 2026 14:35
Copy link
Copy Markdown
Collaborator

@evnchn evnchn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated test, comment, and PR description all show that me, you, test suite, and reality, all align 4-way.

@falkoschindler falkoschindler added this pull request to the merge queue Apr 7, 2026
Merged via the queue into main with commit d24b440 Apr 7, 2026
7 checks passed
@falkoschindler falkoschindler deleted the fix/5937-dict-changed-during-iteration branch April 7, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review Status: PR is open and needs review testing Type/scope: Pytests and test fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Testing click() gives "RuntimeError: dictionary changed size during iteration"

2 participants