bUnit-dev / bUnit

bUnit is a testing library for Blazor components that make tests look, feel, and runs like regular unit tests. bUnit makes it easy to render and control a component under test’s life-cycle, pass parameter and inject services into it, trigger event handlers, and verify the rendered markup from the component using a built-in semantic HTML comparer.
https://bunit.dev
MIT License
1.13k stars 105 forks source link

Throws System.InvalidOperationException when doing Change method #529

Closed bfmsoft closed 2 years ago

bfmsoft commented 2 years ago

Describe the bug

It is entirely possible I don't understand. But, I think this should work? Is there something else I need to do? I have created a small sample project that shows the issue.

Example: Please see sample project. 1) Run the one test in the test project. It will throw

With this test: See Sample

Results in this output: estProject1.RazorTestComponents.InputeSelectEnumTest.RenderDefault  Source: InputeSelectEnumTest.razor line 8  Duration: 446 ms

Message:  System.InvalidOperationException : The render handle is not yet assigned.

Stack Trace:  RenderHandle.ThrowNotInitialized() RenderHandle.Render(RenderFragment renderFragment) ComponentBase.StateHasChanged() IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, Object arg) EventCallback1.InvokeAsync(TValue arg) InputBase1.set_CurrentValue(TValue value) InputBase1.set_CurrentValueAsString(String value) InputSelectEnum1.b0_0(String value) line 19 <>c__DisplayClass32_0`1.b0(ChangeEventArgs e) --- End of stack trace from previous location --- ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task) Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState) TestRenderer.AssertNoUnhandledExceptions() line 350 TestRenderer.DispatchEventAsync(UInt64 eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs) line 93 TriggerEventDispatchExtensions.TriggerNonBubblingEventAsync(ITestRenderer renderer, IElement element, String eventName, EventArgs eventArgs) line 99 TriggerEventDispatchExtensions.TriggerEventAsync(IElement element, String eventName, EventArgs eventArgs) line 55 InputEventDispatchExtensions.ChangeAsync(IElement element, ChangeEventArgs eventArgs) line 42 InputEventDispatchExtensions.Change[T](IElement element, T value) line 24 InputeSelectEnumTest.RenderDefault() line 29

Expected behavior:

Test should pass

Version info:

*Additional context: BlazorApp2.zip *

egil commented 2 years ago

Took me a little bit to figure out what was going on.

So when you are using a .razor component to run your tests from, and don't inherit from TestContext, then the <InputeSelectEnumTest> test component inherits by default from ComponentBase, but since the test component is never instantiated by the renderer, but by the test runner, it doesn't have a render handler attached.

In this particular case, you created a bind-Value that captures the test component as the "receiver" of the event callback when the value changes, and the logic in Blazor sees that it implements ComponentBase, so it expects that it should be re-rendered through a call to StateHasChanged, but that clearly doesn't work for the <InputeSelectEnumTest> test component.

This is something I need to document, so thanks for bringing this to my attention.

My recommended solution is to add @inherits TestContext to the top of your test component, e.g.:

@inherits TestContext
@using BlazorApp2.Components
@using TestProject1
@using static TestProject1.TestEnums
@code {
    private TestData TestData { get; set; } = new TestData();

    [Fact]
    public void RenderDefault() {
        var renderedComponent = Render<InputSelectEnum<Country>>(
            @<EditForm Model="TestData" >
                <DataAnnotationsValidator />
                <InputSelectEnum Id="TestCountryId"
                    @bind-Value="TestData.TestCountry" />
                <ValidationSummary></ValidationSummary>
            </EditForm>
        );

        renderedComponent.MarkupMatches(
            @<select id="TestCountryId" class="valid" value="Default" >
                <option value="Default" selected="">Default</option>
                <option value="UnitedStates">United states</option>
            </select>
        );

        TestData.TestCountry = Country.UnitedStates;
        var x = renderedComponent.Find("#TestCountryId");
        x.Change("UnitedStates");
        renderedComponent.Render();
        renderedComponent.MarkupMatches(
            @<div class="form-control-wrapper">
                <select id="TestCountryId" class="form-control valid" >
                    <option value="0">Default</option>
                    <option value="1" selected="">United States</option>
                </select>
            </div>
        );
    }
}

This allows you to just access all the TestContext methods directly instead of through a ctx variable, which makes the code a little simpler (I think), and there is no need to instantiate the test context and dispose if it in every test. That is handled implicitly for you by xUnit.

If you prefer not to inherit from TestContext, you have to inherit from another class. It can just be an almost empty one, like this one:

public class MyTestBase
{
    protected virtual void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
}