Fix Shadow DOM input binding by using composedPath()#65609
Fix Shadow DOM input binding by using composedPath()#65609ilonatommy wants to merge 2 commits intodotnet:mainfrom
Conversation
Introduce a set of GitHub "skills" and test helpers for an end-to-end fix workflow. Adds ai-summary-comment skill (SKILL.md) and a post-ai-summary-comment.sh script that builds/upserts a single unified AI Summary comment with nested <details> sections, dry-run support, and automatic state loading from CustomAgentLogsTmp/PRState. Adds a comprehensive fix-issue skill (SKILL.md) documenting a 4-phase workflow (pre-flight, test, gate, mandatory multi-model fix) plus try-fix, verify-tests, and write-tests skill definitions. Includes test scripts under fix-issue/tests to validate comment structure, skill definitions, and dry-run previews. These changes enable standardized outputs for automated agents and a consistent PR comment/reporting mechanism for multi-model exploration and fixes.
When an input event originates from inside a Shadow DOM, event.target returns the shadow host element instead of the actual <input> element. This caused EventFieldInfo.fromEvent() and parseChangeEvent() to fail to identify the element as a form field, breaking two-way data binding. Use event.composedPath()[0] when event.composed is true to get the original target element that initiated the event inside the shadow DOM. Fixes dotnet#60885 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates Blazor’s client-side event processing to correctly identify the originating form field for events raised from inside a Shadow DOM (via composedPath()), and adds a new E2E regression test using a custom element with an inner <input> in an open shadow root.
Changes:
- Add a BasicTestApp custom element (
<shadow-input>) whose value is backed by an<input>inside Shadow DOM, plus a test component that binds to it. - Add an E2E test verifying
@bindwithoninputworks when the real input is inside Shadow DOM. - Update Web.JS event parsing to use
event.composedPath()[0](when applicable) instead ofevent.targetfor field/value extraction.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Components/test/testassets/BasicTestApp/wwwroot/js/shadowInputElement.js | Adds a Shadow DOM custom element used by the new regression test. |
| src/Components/test/testassets/BasicTestApp/wwwroot/index.html | Loads the new custom element script in the BasicTestApp harness. |
| src/Components/test/testassets/BasicTestApp/ShadowDomInputComponent.razor | New test component binding to the shadow-hosted input via oninput. |
| src/Components/test/E2ETest/Tests/EventTest.cs | Adds an E2E test covering Shadow DOM input binding behavior. |
| src/Components/Web.JS/src/Rendering/Events/EventTypes.ts | Uses composedPath() for input/change event value parsing to avoid Shadow DOM retargeting issues. |
| src/Components/Web.JS/src/Rendering/Events/EventFieldInfo.ts | Uses composedPath() for reverse-mapping event field info when Shadow DOM retargeting occurs. |
| .github/skills/write-tests/SKILL.md | Adds a new “write-tests” skill definition (workflow/docs). |
| .github/skills/verify-tests/SKILL.md | Adds a new “verify-tests” skill definition (workflow/docs). |
| .github/skills/try-fix/SKILL.md | Adds a new “try-fix” skill definition (workflow/docs). |
| .github/skills/fix-issue/tests/test-skill-definition.sh | Adds bash tests for the new fix-issue skill definition. |
| .github/skills/fix-issue/tests/test-ai-summary-comment.sh | Adds bash tests for AI summary comment scripting. |
| .github/skills/fix-issue/SKILL.md | Adds a comprehensive “fix-issue” skill definition. |
| .github/skills/ai-summary-comment/scripts/post-ai-summary-comment.sh | Adds a script to set PR body content from phase output files. |
| .github/skills/ai-summary-comment/SKILL.md | Adds documentation for the ai-summary-comment skill. |
| 4. Save approach to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/approach.md | ||
| 5. Save result to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/result.txt | ||
| 6. Save diff: git diff > CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/fix.diff |
There was a problem hiding this comment.
The fix-issue instructions write try-fix outputs under CustomAgentLogsTmp/PRState/{PRNumber}/..., but the ai-summary-comment tooling (and docs) load from CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/.... This inconsistency will cause the summary scripts to miss phase outputs depending on which path the agent follows. Align the fix-issue workflow examples to the same directory key (ISSUE-... or PR-...) and update scripts/docs accordingly.
| 4. Save approach to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/approach.md | |
| 5. Save result to CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/result.txt | |
| 6. Save diff: git diff > CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix/attempt-{N}/fix.diff | |
| 4. Save approach to CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix/attempt-{N}/approach.md | |
| 5. Save result to CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix/attempt-{N}/result.txt | |
| 6. Save diff: git diff > CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix/attempt-{N}/fix.diff |
| ### Output File | ||
|
|
||
| ```bash | ||
| mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix |
There was a problem hiding this comment.
The output path example here uses CustomAgentLogsTmp/PRState/{PRNumber}/..., but the ai-summary-comment scripts/documentation read from CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/.... Using different directory keys across phases will break the final PR body generation unless everything is kept in sync.
| mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/try-fix | |
| mkdir -p CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/try-fix |
| ### Output File | ||
|
|
||
| ```bash | ||
| mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/finalize |
There was a problem hiding this comment.
This finalize output path uses CustomAgentLogsTmp/PRState/{PRNumber}/..., while the ai-summary-comment scripts load phase outputs from CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/.... Pick one convention (ISSUE-... seems to match the scripts) and make the fix-issue instructions consistent throughout.
| mkdir -p CustomAgentLogsTmp/PRState/{PRNumber}/PRAgent/finalize | |
| mkdir -p CustomAgentLogsTmp/PRState/ISSUE-{IssueNumber}/PRAgent/finalize |
| --- | ||
| name: fix-issue | ||
| description: End-to-end issue fixer for dotnet/aspnetcore. Runs 5 phases — Pre-Flight, Test, Gate, Fix, Finalize — creating tests and always generating phase output files. Use when asked to "fix issue #XXXXX", "fix #XXXXX", or "work on issue #XXXXX". | ||
| --- | ||
|
|
||
| # Fix Issue — Streamlined 5-Phase Workflow | ||
|
|
||
| End-to-end agent that takes a GitHub issue from investigation through to a completed PR. |
There was a problem hiding this comment.
This PR is titled as a Shadow DOM input binding fix, but it also introduces a large set of new .github/skills/* definitions and scripts (fix-issue, write-tests, etc.). Consider splitting the workflow/skills additions into a separate PR to keep scope focused and make review/rollback safer.
| var shadowRoot = (ShadowRoot)((IJavaScriptExecutor)Browser) | ||
| .ExecuteScript("return arguments[0].shadowRoot", shadowHost); |
There was a problem hiding this comment.
Other E2E tests in this repo use Selenium’s GetShadowRoot() API to access open shadow roots. Using ExecuteScript("return arguments[0].shadowRoot") + cast is less consistent and may be more brittle across drivers; consider switching to shadowHost.GetShadowRoot() for consistency with the rest of the suite.
| var shadowRoot = (ShadowRoot)((IJavaScriptExecutor)Browser) | |
| .ExecuteScript("return arguments[0].shadowRoot", shadowHost); | |
| var shadowRoot = shadowHost.GetShadowRoot(); |
| OUTPUT="$(DRY_RUN=1 bash "$TRY_FIX_SCRIPT" 99999 2>&1)" || true | ||
| EXIT_CODE=$? |
There was a problem hiding this comment.
This test captures the exit code incorrectly: OUTPUT="$(...)" || true makes $? always 0, so failures in the try-fix script won’t be detected. Capture the exit code before forcing success (e.g., temporarily disable set -e around the command, or avoid || true).
| OUTPUT="$(DRY_RUN=1 bash "$TRY_FIX_SCRIPT" 99999 2>&1)" || true | |
| EXIT_CODE=$? | |
| set +e | |
| OUTPUT="$(DRY_RUN=1 bash "$TRY_FIX_SCRIPT" 99999 2>&1)" | |
| EXIT_CODE=$? | |
| set -e |
|
Thank you @ilonatommy for handling the tests with Copilot I couldn’t write them one year ago and gave up, it seems easier to do now with IA so thank for your contribution 👍 |
🤖 AI Summary
📋 Pre-Flight
Issue: #60885 - [Blazor] InputText binding is not supported well with Shadow DOM elements
Area: area-blazor (
src/Components/)PR: None — will create
Key Findings
oninput),event.targetreturns the shadow host element instead of the actual<input>element inside the shadow DOMEventFieldInfo.fromEvent()to fail to identify the input element, returning null, which breaks the binding update timingparseChangeEvent()function inEventTypes.tshas the same issue — usesevent.targetwhich returns the shadow hostevent.composedPath()[0]whenevent.composedis true, which returns the actual element that originated the event inside the shadow DOMTeam Guidance
composedPathinstead ofevent.target, that's fineTest Command
Fix Candidates
event.composed ? event.composedPath().find(_ => true) : event.targetEventFieldInfo.ts,EventTypes.ts🧪 Test
Test Result: ✅ TESTS CREATED
Test Command:
dotnet test src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-build --filter "FullyQualifiedName~InputEvent_WorksWithShadowDomInput"Tests Created:
src/Components/test/E2ETest/Tests/EventTest.cs—InputEvent_WorksWithShadowDomInputsrc/Components/test/testassets/BasicTestApp/ShadowDomInputComponent.razor— Test component with shadow DOM input bindingsrc/Components/test/testassets/BasicTestApp/wwwroot/js/shadowInputElement.js— Web component defining<shadow-input>with shadow DOMTests Written
InputEvent_WorksWithShadowDomInput— Verifies that typing in an<input>inside a shadow DOM correctly updates the Blazor bound value viaoninputeventConclusion
The test creates a
<shadow-input>web component with a shadow DOM containing an<input>. It types characters into the shadow input and verifies the bound value updates correctly. Without the fix,EventFieldInfo.fromEventreturns null for shadow DOM events becauseevent.targetreturns the shadow host (notHTMLInputElement), breaking the binding update mechanism.🚦 Gate
Gate Result: ✅ PASSED
Test Command:
dotnet build src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj --no-restore -v:qBuild Verification
New Tests vs Buggy Code
InputEvent_WorksWithShadowDomInput: Expected to FAIL without fix (event.target returns shadow host, not HTMLInputElement)Regression Check
Conclusion
Tests are properly written, build succeeds. The test will fail without the fix because
EventFieldInfo.fromEventusesevent.targetwhich returns the shadow host element, not the inner<input>. With the fix usingcomposedPath()[0], the correct element will be returned.🔧 Fix
Fix Exploration Summary
Total Attempts: 5
Passing Candidates: 5
Selected Fix: Attempt 1 — Inline ternary using
event.composedPath()[0]Attempt Results
event.composed ? event.composedPath()[0] : event.targetEventUtilities.tswithgetEventTarget()helperCross-Pollination
Exhausted: Yes (cross-pollination ideas were theoretical/complex — not worth implementing)
Comparison
Recommendation
Fix 1 is the best choice because:
composedPath()[0]is the standard way to get the original event targetevent.composedis false for non-shadow DOM events, so existing behavior is unchanged