Skip to content

Commit f97325b

Browse files
Add E2E test cases for events during batch rendering
1 parent a7ebaef commit f97325b

File tree

7 files changed

+134
-4
lines changed

7 files changed

+134
-4
lines changed

src/Components/test/E2ETest/Tests/EventTest.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,37 @@ public void InteractiveElementWithDisabledAttributeDoesNotRespondToMouseEvents(s
239239
Browser.Equal("Got event on enabled button", () => eventLog.GetAttribute("value"));
240240
}
241241

242+
[Fact]
243+
public void EventDuringBatchRendering_CanTriggerDOMEvents()
244+
{
245+
Browser.MountTestComponent<EventDuringBatchRendering>();
246+
247+
var input = Browser.FindElements(By.CssSelector("#reversible-list input"))[0];
248+
var eventLog = Browser.FindElement(By.Id("event-log"));
249+
250+
SendKeysSequentially(input, "abc");
251+
Browser.Equal("abc", () => input.GetAttribute("value"));
252+
Browser.Equal(
253+
"Change event on item First with value a\n" +
254+
"Change event on item First with value ab\n" +
255+
"Change event on item First with value abc",
256+
() => eventLog.Text.Trim().Replace("\r\n", "\n"));
257+
}
258+
259+
[Fact]
260+
public void EventDuringBatchRendering_CannotTriggerJSInterop()
261+
{
262+
Browser.MountTestComponent<EventDuringBatchRendering>();
263+
var errorLog = Browser.FindElement(By.Id("web-component-error-log"));
264+
265+
Browser.FindElement(By.Id("add-web-component")).Click();
266+
var expectedMessage = _serverFixture.ExecutionMode == ExecutionMode.Client
267+
? "Assertion failed - heap is currently locked"
268+
: "There was an exception invoking 'SomeMethodThatDoesntNeedToExistForThisTest' on assembly 'SomeAssembly'";
269+
270+
Browser.Contains(expectedMessage, () => errorLog.Text);
271+
}
272+
242273
void SendKeysSequentially(IWebElement target, string text)
243274
{
244275
// Calling it for each character works around some chars being skipped
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<h1>Event during batch rendering</h1>
2+
3+
<p>
4+
While Blazor WebAssembly is rendering a batch, the JavaScript code reads data from the .NET heap directly.
5+
So, it's essential that .NET code doesn't run during this time (either to modify the state of the
6+
render tree or to perform garbage collection which may relocate objects in the heap).
7+
</p>
8+
<p>
9+
To ensure this is safe, batch rendering is a fully synchronous process during which the JS code doesn't
10+
yield control to user code. However, there are possible cases where user code may be triggered unavoidably
11+
including (1) JavaScript DOM mutation observers, (2) Web Component lifecycle events, and (3) edge cases
12+
where Blazor performing a DOM mutation can itself trigger a .NET-bound event such as "change".
13+
</p>
14+
<p>
15+
Cases (1) and (2) result in developer-supplied JS code executing, which may try to perform .NET interop.
16+
The intended behavior is that .NET interop calls should be blocked while a batch is being rendered.
17+
Developers need to wrap such calls in <code>requestAnimationFrame</code> or <code>setTimeout(..., 0)</code>
18+
or similar, so that it runs after the current batch has finished rendering.
19+
</p>
20+
<p>
21+
Case (3) more directly results in developer-supplied .NET code executing. The intended behavior in this case
22+
is that Blazor takes care of deferring the event dispatch until the current batch finishes rendering. This
23+
shouldn't be regarded as problematic, because Blazor has never guaranteed synchronous dispatch of DOM event
24+
handlers (in the Blazor Server case, all DOM event handlers run asynchronously).
25+
</p>
26+
27+
<h2>WebComponent attempting JS interop during batch rendering (cases 1 & 2 above)</h2>
28+
29+
@for (var i = 0; i < numWebComponents; i++)
30+
{
31+
<custom-web-component-performing-js-interop>Instance @i</custom-web-component-performing-js-interop>
32+
}
33+
34+
<button id="add-web-component" @onclick="@(() => numWebComponents++)">Add a web component</button>
35+
36+
<pre id="web-component-error-log"></pre>
37+
38+
<h2>DOM mutation triggering a .NET event handler (case 3 above)</h2>
39+
40+
<p>
41+
Type into either text box. Each keystroke will swap the list order, causing a change event during batch rendering.
42+
</p>
43+
44+
<div id="reversible-list">
45+
@foreach (var item in itemsList)
46+
{
47+
<div @key="item">
48+
<input @oninput="@(() => itemsList.Reverse())"
49+
@onchange="@(evt => eventLog += $"Change event on item {item.Name} with value {evt.Value}\n")" />
50+
</div>
51+
}
52+
</div>
53+
54+
<pre id="event-log">@eventLog</pre>
55+
56+
@code {
57+
string eventLog = "";
58+
int numWebComponents = 0;
59+
60+
class ListItem
61+
{
62+
public string Name { get; set; }
63+
}
64+
65+
List<ListItem> itemsList = new List<ListItem>
66+
{
67+
new ListItem { Name = "First" },
68+
new ListItem { Name = "Second" },
69+
};
70+
}

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
2828
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
2929
<option value="BasicTestApp.EventDisablingComponent">Event disabling</option>
30+
<option value="BasicTestApp.EventDuringBatchRendering">Event during batch rendering</option>
3031
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
3132
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
3233
<option value="BasicTestApp.FocusEventComponent">Focus events</option>

src/Components/test/testassets/BasicTestApp/wwwroot/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
<a class='dismiss' style="cursor: pointer;">🗙</a>
2222
</div>
2323

24-
<!-- Used for testing interop scenarios between JS and .NET -->
24+
<!-- Used for specific test cases -->
2525
<script src="js/jsinteroptests.js"></script>
26+
<script src="js/webComponentPerformingJsInterop.js"></script>
2627

2728
<script>
2829
// Used by ElementRefComponent
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// This web component is used from the EventDuringBatchRendering test case
2+
3+
window.customElements.define('custom-web-component-performing-js-interop', class extends HTMLElement {
4+
connectedCallback() {
5+
this.attachShadow({ mode: 'open' });
6+
this.shadowRoot.innerHTML = `
7+
<div style='border: 2px dashed red; margin: 10px 0; padding: 5px; background: #dddddd;'>
8+
<slot></slot>
9+
</div>
10+
`;
11+
12+
// Since this happens during batch rendering, it will be blocked.
13+
// In the future we could allow async calls, but this is enough of an edge case
14+
// that it doesn't need to be implemented currently. Developers who need to do this
15+
// can wrap their interop call in requestAnimationFrame or setTimeout(..., 0).
16+
(async function () {
17+
try {
18+
await DotNet.invokeMethodAsync('SomeAssembly', 'SomeMethodThatDoesntNeedToExistForThisTest');
19+
} catch (ex) {
20+
document.getElementById('web-component-error-log').innerText += ex.toString() + '\n';
21+
}
22+
})();
23+
}
24+
});

src/Components/test/testassets/TestServer/Components.TestServer.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,17 @@
3434

3535
<Target Name="CopyClientAssetsForTest" BeforeTargets="Build"
3636
Inputs="..\BasicTestApp\wwwroot\js\jsinteroptests.js;
37+
..\BasicTestApp\wwwroot\js\webComponentPerformingJsInterop.js;
3738
..\BasicTestApp\wwwroot\NotAComponent.html;
3839
..\BasicTestApp\wwwroot\style.css"
3940
Outputs="wwwroot\js\jsinteroptests.js;
41+
wwwroot\js\webComponentPerformingJsInterop.js;
4042
wwwroot\NotAComponent.html;
4143
wwwroot\style.css">
4244

4345
<MakeDir Directories="wwwroot" />
4446

45-
<Copy SourceFiles="..\BasicTestApp\wwwroot\js\jsinteroptests.js;..\BasicTestApp\wwwroot\NotAComponent.html;..\BasicTestApp\wwwroot\style.css"
46-
DestinationFiles="wwwroot\js\jsinteroptests.js;wwwroot\NotAComponent.html;wwwroot\style.css" />
47+
<Copy SourceFiles="..\BasicTestApp\wwwroot\js\jsinteroptests.js;..\BasicTestApp\wwwroot\js\webComponentPerformingJsInterop.js;..\BasicTestApp\wwwroot\NotAComponent.html;..\BasicTestApp\wwwroot\style.css"
48+
DestinationFiles="wwwroot\js\jsinteroptests.js;wwwroot\js\webComponentPerformingJsInterop.js;wwwroot\NotAComponent.html;wwwroot\style.css" />
4749
</Target>
4850
</Project>

src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
<body>
1515
<root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
1616

17-
<!-- Used for testing interop scenarios between JS and .NET -->
17+
<!-- Used for specific test cases -->
1818
<script src="js/jsinteroptests.js"></script>
19+
<script src="js/webComponentPerformingJsInterop.js"></script>
1920

2021
<div id="blazor-error-ui">
2122
An unhandled error has occurred.

0 commit comments

Comments
 (0)