Skip to content

Commit 18eef22

Browse files
csharpfritzCopilot
andcommitted
fix: wait for Blazor circuit before form interaction in tests
The WebFormsForm Playwright tests were racing against Blazor Server circuit establishment. During prerendering, the form renders without @onsubmit:preventDefault, so clicking submit before the circuit is ready causes a full-page POST that resets component state. Changes: - Add data-interactive attribute to WebFormsForm when in interactive mode - Wait for form[data-interactive] in tests before filling/submitting - Tighten input locators to match actual field names (username, email) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d74257e commit 18eef22

6 files changed

Lines changed: 73 additions & 5 deletions

File tree

.squad/agents/cyclops/history.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,3 +987,12 @@ foreach (var warning in warnings) {
987987
- `@onsubmit:preventDefault` prevents default form submission in interactive mode
988988

989989
**Build:** ✅ 0 errors on net8.0;net9.0;net10.0
990+
991+
### Playwright Locator Fix for WebFormsFormTests (2025-07-15)
992+
993+
**Summary:** Fixed two failing Playwright tests in `WebFormsFormTests.cs` where locators were matching the always-visible migration guidance card instead of the actual results card.
994+
995+
**Key Learning — data-audit-control naming convention:**
996+
- Demo pages use `data-audit-control` attributes prefixed with the component name, e.g. `webforms-form-results` not just `form-results`.
997+
- Always use the exact `[data-audit-control='...']` selector in Playwright tests rather than fuzzy text-based locators like `text=Request.Form` or `:has-text()`, which can match unrelated visible sections.
998+
- When a section is conditionally rendered (e.g., gated by `@if (isPostBack)`), the test locator must target that specific section's attribute, not generic text that may appear elsewhere on the page.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
### 2026-04-07T14-16-06Z: User directive
2+
**By:** Jeffrey T. Fritz (via Copilot)
3+
**What:** Avoid security topics (auth, identity, FormsAuthentication, Membership/Roles) for now - they can get gnarly.
4+
**Why:** User request - captured for team memory
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Decision: FormShim wraps IFormCollection with null-safe degradation
2+
3+
**Author:** Cyclops
4+
**Date:** 2026-07-31
5+
**Status:** Implemented
6+
7+
## Context
8+
9+
Migrated Web Forms code-behind frequently uses `Request.Form["fieldName"]` to read POST data. The existing `RequestShim` had `Cookies`, `QueryString`, and `Url` but no `Form`.
10+
11+
## Decision
12+
13+
Created `FormShim` as a public class (not an interface implementation) that wraps `IFormCollection?` and provides `NameValueCollection`-like API. The `RequestShim.Form` property follows the same graceful degradation pattern as `Cookies`:
14+
15+
1. **SSR mode (HttpContext available):** wraps `HttpContext.Request.Form` via `FormShim`
16+
2. **Non-form-encoded requests:** catches `InvalidOperationException` → returns empty `FormShim(null)`
17+
3. **Interactive mode (no HttpContext):** logs warning once → returns empty `FormShim(null)`
18+
19+
## Why not implement IFormCollection?
20+
21+
`FormShim` targets `NameValueCollection` semantics (Web Forms API), not `IFormCollection` (ASP.NET Core API). The indexer, `GetValues()`, `AllKeys` surface matches what migrated code actually calls. Implementing `IFormCollection` would add unnecessary API surface and file upload concerns.
22+
23+
## Impact
24+
25+
- Unblocks any migrated code-behind that reads `Request.Form["key"]`
26+
- No breaking changes — new property on existing class
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Decision: SSR Opt-Out Pattern via ExcludeFromInteractiveRouting
2+
3+
**Author:** Cyclops
4+
**Date:** 2025-07-24
5+
**Status:** Implemented
6+
7+
## Context
8+
9+
The sample app uses `@rendermode="InteractiveServer"` on `<Routes>` in `App.razor`, meaning all pages run interactively via SignalR. Pages that need real HTTP POST (like the Request.Form demo) can't function because there's no form data in interactive mode.
10+
11+
## Decision
12+
13+
Modified `App.razor` to use `HttpContext.AcceptsInteractiveRouting()` for conditional render mode assignment. Pages that need SSR add `@attribute [ExcludeFromInteractiveRouting]` — the standard Microsoft pattern for .NET 8+.
14+
15+
**Impact:** This is non-breaking. All existing pages continue as InteractiveServer. Only pages explicitly decorated with the attribute will render as SSR.
16+
17+
## Applies To
18+
19+
Any future sample page or migration demo that needs real HTTP request/response semantics (form POSTs, Request.Form, Request.Cookies during POST, etc.) should use this same attribute.

samples/AfterBlazorServerSide.Tests/Migration/WebFormsFormTests.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ public async Task WebFormsForm_InitialLoad_ShowsFormWithoutResults()
7777
});
7878

7979
// Form inputs should be visible (name and email)
80-
var nameInput = page.Locator("input[name='name'], input#name, input[id*='name' i]").First;
80+
var nameInput = page.Locator("input[name='username'], input#username").First;
8181
await Assertions.Expect(nameInput).ToBeVisibleAsync();
8282

83-
var emailInput = page.Locator("input[name='email'], input#email, input[id*='email' i]").First;
83+
var emailInput = page.Locator("input[name='email'], input#email").First;
8484
await Assertions.Expect(emailInput).ToBeVisibleAsync();
8585

8686
// Submit button should be visible
@@ -115,13 +115,19 @@ public async Task WebFormsForm_SubmitForm_ShowsRequestFormValues()
115115
Timeout = 30000
116116
});
117117

118+
// Wait for Blazor Server interactive circuit — the WebFormsForm component
119+
// renders data-interactive="true" only after the circuit is established.
120+
// Without this wait, the prerendered form lacks @onsubmit:preventDefault
121+
// and clicking submit causes a full-page POST that resets component state.
122+
await page.WaitForSelectorAsync("form[data-interactive]", new() { Timeout = 10000 });
123+
118124
// Fill in the name field
119-
var nameInput = page.Locator("input[name='name'], input#name, input[id*='name' i]").First;
125+
var nameInput = page.Locator("input[name='username'], input#username").First;
120126
await nameInput.FillAsync("TestUser");
121127
await page.Keyboard.PressAsync("Tab");
122128

123129
// Fill in the email field
124-
var emailInput = page.Locator("input[name='email'], input#email, input[id*='email' i]").First;
130+
var emailInput = page.Locator("input[name='email'], input#email").First;
125131
await emailInput.FillAsync("test@example.com");
126132
await page.Keyboard.PressAsync("Tab");
127133

@@ -167,8 +173,11 @@ public async Task WebFormsForm_SubmitForm_StaysOnSamePage()
167173
Timeout = 30000
168174
});
169175

176+
// Wait for Blazor Server interactive circuit
177+
await page.WaitForSelectorAsync("form[data-interactive]", new() { Timeout = 10000 });
178+
170179
// Fill a field and submit
171-
var nameInput = page.Locator("input[name='name'], input#name, input[id*='name' i]").First;
180+
var nameInput = page.Locator("input[name='username'], input#username").First;
172181
await nameInput.FillAsync("NavigationTest");
173182
await page.Keyboard.PressAsync("Tab");
174183

src/BlazorWebFormsComponents/WebFormsForm.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<form @ref="_formRef"
1212
method="@MethodString"
1313
action="@Action"
14+
data-interactive="true"
1415
@onsubmit="HandleSubmit"
1516
@onsubmit:preventDefault
1617
@attributes="HtmlAttributes">

0 commit comments

Comments
 (0)