Skip to content

Component does not re-render if InputFile control included in Razor markup #314

Closed

Description

@mrakestraw-bbins

Describe the bug

Per the razor component below, simply add an InputFile tag to your markup and Bunit will not be able to re-render the component upon state change. Sometimes the state will be updated and sometimes not, but never is the markup updated so that you can find markup elements in the else condition of the page.

Note that the input tag works fine.

For reference: Microsoft.AspnetCore.Components.Forms.InputFile

Also, occasionally, I saw a Bunit recursive render stack overflow, but that seemed to be caused by the await LoadForecasts(); in the OnInitializedAsync method. If I see that happen again, I'll try to recreate and open a different bug report.

Example:
Testing this component: WeatherDisplay.razor

@if (Forecasts == null)
{
    <p id="LoadingMessage"><em>Loading...</em></p>
}
else
{
    <InputFile Id="sampleInputFileIdbad" />
    <input type="file" id="sampleInputFileId" />
    <table id="forecasttable" class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in Forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}


@code {

    [Inject]
    public HttpClient Http { get; set; }

    public List<WeatherForecast> Forecasts;

    protected override async Task OnInitializedAsync()
    {
        await LoadForecasts();
    }

    public async Task LoadForecasts()
    {
        var results = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
        Forecasts = results.ToList();
    }

    public class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public string Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

With this test:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BBINS.Shared;
using Bunit;
using RichardSzalay.MockHttp;
using Xunit;

namespace Client.Test
{
    public class WeatherDisplayTests : TestContext
    {
        public WeatherDisplayTests()
        {
            var mock = Services.AddMockHttpClient();
            mock.When("/sample-data/weather.json").RespondJson(new List<WeatherDisplay.WeatherForecast> {
                new()
                {
                    Date = new DateTime(1990, 12, 31),
                    Summary = "Freezing",
                    TemperatureC = 1
                },
                new()
                {
                    Date = new DateTime(2001, 12, 31),
                    Summary = "Freezing",
                    TemperatureC = 2
                }
            });
        }

        [Fact]
        public void WeatherDisplayInitializeTest()
        {
            var cut = RenderComponent<WeatherDisplay>();

            // Is this a race condition?
            Assert.Equal("Loading...", cut.Find("p#LoadingMessage").TextContent);
        }

        [Fact]
        public void WeatherDisplayInitializeTest2()
        {
            var cut = RenderComponent<WeatherDisplay>();
            cut.WaitForState(() => cut.Instance.Forecasts != null);

            // Will not find the table if InputFile included in Razor
            cut.WaitForAssertion(() => cut.Find("#forecasttable"));
        }
    }
}

Results in this output:

C:\src\BBINS-bUnit>dotnet test
  Determining projects to restore...
  All projects are up-to-date for restore.
  BBINS-bUnit -> C:\src\BBINS-bUnit\BBINS-bUnit\bin\Debug\net5.0\BBINS.dll
  BBINS-bUnit (Blazor output) -> C:\src\BBINS-bUnit\BBINS-bUnit\bin\Debug\net5.0\wwwroot
  Client.Test -> C:\src\BBINS-bUnit\Client.Test\bin\Debug\net5.0\Client.Test.dll
Test run for C:\src\BBINS-bUnit\Client.Test\bin\Debug\net5.0\Client.Test.dll (.NETCoreApp,Version=v5.0)
Microsoft (R) Test Execution Command Line Tool Version 16.8.3
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:02.43]     Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2 [FAIL]
  Failed Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2 [1 s]
  Error Message:
   Bunit.Extensions.WaitForHelpers.WaitForFailedException : The state predicate did not pass before the timeout period passed.
  Stack Trace:
     at Bunit.RenderedFragmentWaitForHelperExtensions.WaitForState(IRenderedFragmentBase renderedFragment, Func`1 statePredicate, Nullable`1 timeout) in /_/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs:line 31
   at Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2() in C:\src\BBINS-bUnit\Client.Test\WeatherDisplayTests.cs:line 45

Failed!  - Failed:     1, Passed:     1, Skipped:     0, Total:     2, Duration: 1 s - Client.Test.dll (net5.0)

Expected behavior:
If I comment out the <InputFile /> tag, I get:

C:\src\BBINS-bUnit>dotnet test
  Determining projects to restore...
  All projects are up-to-date for restore.
  BBINS-bUnit -> C:\src\BBINS-bUnit\BBINS-bUnit\bin\Debug\net5.0\BBINS.dll
  BBINS-bUnit (Blazor output) -> C:\src\BBINS-bUnit\BBINS-bUnit\bin\Debug\net5.0\wwwroot
  Client.Test -> C:\src\BBINS-bUnit\Client.Test\bin\Debug\net5.0\Client.Test.dll
Test run for C:\src\BBINS-bUnit\Client.Test\bin\Debug\net5.0\Client.Test.dll (.NETCoreApp,Version=v5.0)
Microsoft (R) Test Execution Command Line Tool Version 16.8.3
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 523 ms - Client.Test.dll (net5.0)

Version info: Test Project

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="bunit.web" Version="1.0.0-preview-01" />
    <PackageReference Include="bunit.xunit" Version="1.0.0-preview-01" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="1.3.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\BBINS-bUnit\BBINS-bUnit.csproj" />
  </ItemGroup>

</Project>

Version Info: Blazor WASM project

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <AssemblyName>BBINS</AssemblyName>
    <RootNamespace>BBINS</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.2" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.2" PrivateAssets="all" />
    <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
  </ItemGroup>

</Project>
C:\src\BBINS-bUnit>dotnet --info
.NET SDK (reflecting any global.json):
 Version:   5.0.102
 Commit:    71365b4d42

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18363
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\5.0.102\

Host (useful for support):
  Version: 5.0.2
  Commit:  cb5f173b96

.NET SDKs installed:
  3.1.405 [C:\Program Files\dotnet\sdk]
  5.0.101 [C:\Program Files\dotnet\sdk]
  5.0.102 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.All 2.1.24 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.24 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.10 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.24 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.10 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.1.10 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Activity

  1. mrakestraw-bbins commented on Feb 4, 2021

    @mrakestraw-bbins
    Author

    Forgot to include project files:
    BBINS-bUnit.zip

  2. egil commented on Feb 5, 2021

    @egil
    Member

    First of, excellent bug report writing, thanks for adding all the details @mrakestraw-bbins 🥇🥇

    I just did a read through the InputFile component, and it has some JSInterop magic going on, which might be causing the problems... I wonder if this is related to preview-01 adding support for IJSUnmarshalledRuntime.

    This should be a fun one investigating.

    And just to clarify, when awaiting something that just returns a completed task immediately, the usage of cut.WaitForAssertion is not needed, as the Blazor renderer will simply re-render right away without yielding back to the test thread.

  3. added
    bugSomething isn't working
    help wantedExtra attention is needed
    investigateThis issue require further investigation before closing.
    on Feb 5, 2021
  4. mrakestraw-bbins commented on Feb 5, 2021

    @mrakestraw-bbins
    Author

    Thanks @egil. Nice library by the way. Wish I had more time to dig in and try to patch it myself.

  5. egil commented on Feb 5, 2021

    @egil
    Member

    This is probably related to this error that sometimes happens when running the tests here in GitHub Actions:

    [xUnit.net 00:00:06.21]     Bunit.BlazorE2E.ComponentRenderingTest.CanDispatchAsyncWorkToSyncContext [FAIL]
      Failed Bunit.BlazorE2E.ComponentRenderingTest.CanDispatchAsyncWorkToSyncContext [2 s]
      Error Message:
       Bunit.Extensions.WaitForHelpers.WaitForFailedException : The assertion did not pass within the timeout period.
    ---- Assert.Equal() Failure
                    ↓ (pos 6)
    Expected: First Second Third Fourth Fifth
    Actual:   First Third Second Fourth Fifth
                    ↑ (pos 6)
      Stack Trace:
         at Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(IRenderedFragmentBase renderedFragment, Action assertion, Nullable`1 timeout) in /home/runner/work/bUnit/bUnit/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs:line 53
       at Bunit.BlazorE2E.ComponentRenderingTest.CanDispatchAsyncWorkToSyncContext() in /home/runner/work/bUnit/bUnit/tests/bunit.web.tests/BlazorE2E/ComponentRenderingTest.cs:line 561
    ----- Inner Stack Trace -----
       at Bunit.BlazorE2E.ComponentRenderingTest.<>c__DisplayClass30_0.<CanDispatchAsyncWorkToSyncContext>b__0() in /home/runner/work/bUnit/bUnit/tests/bunit.web.tests/BlazorE2E/ComponentRenderingTest.cs:line 566
       at Bunit.Extensions.WaitForHelpers.WaitForAssertionHelper.<>c__DisplayClass7_0.<.ctor>b__0() in /home/runner/work/bUnit/bUnit/src/bunit.core/Extensions/WaitForHelpers/WaitForAssertionHelper.cs:line 33
       at Bunit.Extensions.WaitForHelpers.WaitForHelper.OnAfterRender(Object sender, EventArgs args) in /home/runner/work/bUnit/bUnit/src/bunit.core/Extensions/WaitForHelpers/WaitForHelper.cs:line 89
    {"Key":"a","Code":null,"Location":0,"Repeat":false,"CtrlKey":false,"ShiftKey":false,"AltKey":false,"MetaKey":false,"Type":null}
    {"Key":"b","Code":null,"Location":0,"Repeat":false,"CtrlKey":false,"ShiftKey":false,"AltKey":false,"MetaKey":false,"Type":null}
    

    I know this test used to work all the time, so I will have to investigate where a possible regression has been introduced. Gotta love those subtle race condition bugs 😒

  6. egil commented on Feb 6, 2021

    @egil
    Member

    @mrakestraw-bbins, if I reduce the test case even more, we get a different exception/error, which I actually thing is the root cause, because the error indicate a missing IOptions<InputFile> is not added to the Services collection. It is a bug that the error does not show up in the test output though, but I think that is due to it happening async on the other thread. With the test case below, we force the exception to happen essentially while the test thread and the renderer thread are the same.

    Our CUT:

    @if (!show)
    {
        <div id="loading"/>
    }
    else
    {
        <InputFile Id="sampleInputFileIdbad" />
        <div id="showing"/>
    }
    @code {
        bool show = false;
        protected override async Task OnInitializedAsync()
        {
            await Task.CompletedTask;
            show = true;
        }
    }

    The test:

    [Fact]
    public async Task WeatherDisplayInitializeTest2()
    {
        var cut = RenderComponent<WeatherDisplay>();
    
        Assert.Equal(1, cut.FindAll("#showing").Count);
    }

    The output:

    Test Name:	Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2
    Test FullName:	Client.Test.Client.Test.WeatherDisplayTests.Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2
    Test Source:	C:\source\temp\BBINS-bUnit\Client.Test\WeatherDisplayTests.cs : line 42
    Test Outcome:	Failed
    Test Duration:	0:00:00
    
    Test Name:	Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2
    Test Outcome:	Failed
    Result StackTrace:	
    at Microsoft.AspNetCore.Components.ComponentFactory.<>c__DisplayClass6_0.<CreateInitializer>g__Initialize|2(IServiceProvider serviceProvider, IComponent component)
       at Microsoft.AspNetCore.Components.ComponentFactory.PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
       at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
       at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateComponent(Type componentType)
       at Microsoft.AspNetCore.Components.RenderTree.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame& frame, Int32 parentComponentId)
       at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex)
       at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex)
       at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex)
       at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
       at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange`1 oldTree, ArrayRange`1 newTree)
       at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
       at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
       at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
    --- End of stack trace from previous location ---
       at Bunit.Rendering.TestRenderer.AssertNoUnhandledExceptions() in /_/src/bunit.core/Rendering/TestRenderer.cs:line 280
       at Bunit.Rendering.TestRenderer.Render[TResult](RenderFragment renderFragment, Func`2 activator) in /_/src/bunit.core/Rendering/TestRenderer.cs:line 164
       at Bunit.Rendering.TestRenderer.RenderFragment(RenderFragment renderFragment) in /_/src/bunit.core/Rendering/TestRenderer.cs:line 38
       at Bunit.Extensions.TestContextExtensions.RenderInsideRenderTree[TComponent](TestContextBase testContext, RenderFragment renderFragment) in /_/src/bunit.web/Extensions/TestContextExtensions.cs:line 20
       at Bunit.TestContext.Render[TComponent](RenderFragment renderFragment) in /_/src/bunit.web/TestContext.cs:line 61
       at Bunit.TestContext.RenderComponent[TComponent](ComponentParameter[] parameters) in /_/src/bunit.web/TestContext.cs:line 36
       at Client.Test.WeatherDisplayTests.WeatherDisplayInitializeTest2() in C:\source\temp\BBINS-bUnit\Client.Test\WeatherDisplayTests.cs:line 44
    --- End of stack trace from previous location ---
    Result Message:	System.InvalidOperationException : Cannot provide a value for property 'Options' on type 'Microsoft.AspNetCore.Components.Forms.InputFile'. There is no registered service of type 'Microsoft.Extensions.Options.IOptions`1[Microsoft.AspNetCore.Components.Forms.RemoteBrowserFileStreamOptions]'.
    
  7. egil commented on Feb 6, 2021

    @egil
    Member

    OK, this turns out not to be a race condition or a bug, per say... but more a problem with bUnit not having built in support for the <InputFile> component.

    Here is how to write the test, using a mocking framework of choice, to create a mock of the IOptions<RemoteBrowserFileStreamOptions> that InputFile requires.

    using System;
    using System.Collections.Generic;
    using BBINS.Shared;
    using Bunit;
    using Bunit.JSInterop;
    using Microsoft.AspNetCore.Components.Forms;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;
    using NSubstitute;
    using RichardSzalay.MockHttp;
    using Xunit;
    
    namespace Client.Test
    {
        public class WeatherDisplayTests : TestContext
        {
            public WeatherDisplayTests()
            {
                var mock = Services.AddMockHttpClient();
                mock.When("/sample-data/weather.json").RespondJson(new List<WeatherDisplay.WeatherForecast> {
                    new()
                    {
                        Date = new DateTime(1990, 12, 31),
                        Summary = "Freezing",
                        TemperatureC = 1
                    },
                    new()
                    {
                        Date = new DateTime(2001, 12, 31),
                        Summary = "Freezing",
                        TemperatureC = 2
                    }
                });
    
                var optionsMock = Substitute.For<IOptions<RemoteBrowserFileStreamOptions>>();
                Services.AddSingleton<IOptions<RemoteBrowserFileStreamOptions>>(optionsMock);
                JSInterop.SetupVoid("Blazor._internal.InputFile.init", x => true);
            }
    
            [Fact]
            public void WeatherDisplayInitializeTest()
            {
                var cut = RenderComponent<WeatherDisplay>();
    
                // Is this a race condition?
                Assert.Equal("Loading...", cut.Find("p#LoadingMessage").TextContent);
            }
    
            [Fact]
            public void WeatherDisplayInitializeTest2()
            {
                var cut = RenderComponent<WeatherDisplay>();
                cut.WaitForState(() => cut.Instance.Forecasts != null);
    
                // Will not find the table if InputFile included in Razor
                cut.WaitForAssertion(() => cut.Find("#forecasttable"));
            }
    
            [Fact]
            public void WeatherDisplayInitializeTest3()
            {
                var cut = RenderComponent<WeatherDisplay>();
    
                cut.WaitForAssertion(() => cut.Find("#forecasttable"));
            }
        }
    }
  8. added
    questionFurther information is requested
    and removed
    bugSomething isn't working
    on Feb 6, 2021
  9. locked and limited conversation to collaborators on Feb 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is neededinvestigateThis issue require further investigation before closing.questionFurther information is requested

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions