Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsRuntimePlannedInvocation not behaving as expected together with RenderComponent #78

Closed
johanjonsson1 opened this issue Mar 24, 2020 · 5 comments
Labels
enhancement New feature or request input needed When an issue requires input or suggestions question Further information is requested

Comments

@johanjonsson1
Copy link

I have a test where MyComponent is setting a property once during OnAfterRenderAsync when firstRender is true which it only is when constructing the component using RenderComponent.

When i call Setup and then SetResult with a specified value on the planned invocation for the MockJsRuntime, the expected behaviour is that the value specified is returned when calling InvokeAsync.

MyComponent.razor

@inherits MyComponentBase
Hello from MyComponent

MyComponentBase.cs

    public class MyComponentBase : ComponentBase
    {
        [Inject]
        protected IJSRuntime JsRuntime { get; set; }

        public string? TestProperty { get; private set; }

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
                TestProperty = await JsRuntime.InvokeAsync<string>("func");
        }
    }

And this test case:

        [Fact]
        public void Test()
        {
            var mockJsRuntime = Services.AddMockJsRuntime(JsRuntimeMockMode.Strict);
            var plannedInvocation = mockJsRuntime.Setup<string>("func");
            plannedInvocation.SetResult("result test");

            var cut = RenderComponent<MyComponent>();

            Assert.Equal("result test", cut.Instance.TestProperty);
        }

Results in test case failing

Additional info:
Above test works as expected if I change the RegisterInvocation method in JsRuntimePlannedInvocationBase to also return Completed tasks but that results in other failing unit tests

@egil
Copy link
Member

egil commented Mar 24, 2020

Hi Johan, I'll take a closer look later tonight or tomorrow and get back to you. In the meantime, try wrapping your assertion in cut.WaitForAssertion(() => Assert.Equal("result test", cut.Instance.TestProperty)).

@egil
Copy link
Member

egil commented Mar 26, 2020

@johanjonsson1 hey, thanks for the patients. Here is a bit more feedback:

I have a test where MyComponent is setting a property once during OnAfterRenderAsync when firstRender is true which it only is when constructing the component using RenderComponent.

In general, if you want to test your component when firstRender is false, simply call cut.Render() to cause the component to re-render. That will be a second render, and firstRender will be false. You can also achieve this by triggering an event handler in the component.

As for the issue in general, a planned invocation will only "SetResult" on invocations it has already registered, thus, you have to call SetResult after RenderComponent. You also have to use WaitForAssertion, since the change in MyComponent happens async on the render thread, and the test is running in another thread.

[Fact]
public void Test()
{
    using var ctx = new TestContext();
    var mockJsRuntime = ctx.Services.AddMockJsRuntime(JsRuntimeMockMode.Strict);
    var plannedInvocation = mockJsRuntime.Setup<string>("func");

    var cut = ctx.RenderComponent<MyComponent>();

    plannedInvocation.SetResult("result test"); // Move to after RenderComponent
    cut.WaitForAssertion(() => Assert.Equal("result test", cut.Instance.TestProperty));
}

Last, but not least, to get WaitForAssertion to actually discover the change, you have to trigger a rerender in the component when InvokeAsync returns through StateHasChanged.

class MyComponent : ComponentBase
{
    [Inject]
    protected IJSRuntime JsRuntime { get; set; }

    public string? TestProperty { get; private set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            TestProperty = await JsRuntime.InvokeAsync<string>("func");
            StateHasChanged();
        }
    }
}

Now, admittedly, it took me a while to remember how planned invocation works, so it might be worth considering changing its behaviour.

That would mean you cannot set a new result for later invocation that matches the planned invocation. What do you think?

@egil egil added input needed When an issue requires input or suggestions question Further information is requested labels Mar 26, 2020
@johanjonsson1
Copy link
Author

Thank you for your investigation @egil. The render again thing i'm aware of but my case is actually based on this interop example https://blazor-university.com/javascript-interop/calling-dotnet-from-javascript/lifetimes-and-memory-leaks/

Testing that the correct private GeneratorHandle (in my case set with plannedStartInvocation.SetResult) and thus not requiring a rerender, is passed back to javascript when disposing.

Its working fine by moving the SetResult call and using WaitForState.

            var mockJsRuntime = Services.AddMockJsRuntime(JsRuntimeMockMode.Strict);
            var plannedStartInvocation = mockJsRuntime.Setup<int>("start", args => true);
            var plannedStopInvocation = mockJsRuntime.SetupVoid("stop", args => true);
            var handlerId = 24;

            var cut = RenderComponent<Component>();
            plannedStartInvocation.SetResult(handlerId);
            cut.WaitForState(() => true);
            cut.Instance.Dispose();

            Assert.AreEqual(1, plannedStartInvocation.Invocations.Count, "start should only be called once");
            Assert.AreEqual(1, plannedStopInvocation.Invocations.Count, "stop should be called when disposing");
            Assert.AreEqual(handlerId, plannedStopInvocation.Invocations.First().Arguments.First());

I guess the thing that threw me off was me being use to the AAA pattern and making all setup at the start of the test :)

Also, with your explanation Test006 in MockJsRuntimeInvokeHandlerTest makes much more sense!

Regarding your last question. I did something like that and broke Test006.
An API supporting something like below would in my opinion be somewhat more intuitive. And one can use AAA.

        public async Task Test1()
        {
            var identifier = "func";
            var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict);
            var plannedInvoke = sut.Setup<int>(identifier);
            var jsRuntime = sut.ToJsRuntime();
            plannedInvoke.SetResult(1, mutable: true); // default true?

            var i1 = await jsRuntime.InvokeAsync<int>(identifier);
            i1.ShouldBe(1);

            plannedInvoke.SetResult(2, mutable: true);
            plannedInvoke.SetResult(3, mutable: true);
            plannedInvoke.SetResult(4, mutable: true);
            var i2 = await jsRuntime.InvokeAsync<int>(identifier);
            i2.ShouldBe(4);
        }

        public async Task Test2()
        {
            var identifier = "func";
            var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict);
            var plannedInvoke = sut.Setup<int>(identifier);
            var jsRuntime = sut.ToJsRuntime();
            plannedInvoke.SetResult(1, mutable: false);
            var i1 = await jsRuntime.InvokeAsync<int>(identifier);
            i1.ShouldBe(1);

            plannedInvoke.SetResult(2); // ignored or exception
            var i2 = await jsRuntime.InvokeAsync<int>(identifier);
            i2.ShouldBe(1);
        }

@egil
Copy link
Member

egil commented Mar 27, 2020

I agree. I will try to get that change into the next release.

@egil egil added the enhancement New feature or request label Mar 27, 2020
@egil egil added this to the beta-7 milestone Apr 3, 2020
@egil
Copy link
Member

egil commented May 8, 2020

Just a quick update on this @johanjonsson1.

I decided that JsRuntimePlannedInvocation can now have its result set at any time, and requests will get the result when they arrive, if one is set. A new result can also be provided at any time, but it will only affect requests arriving after it has been set, not the previous.

@egil egil closed this as completed May 8, 2020
egil added a commit that referenced this issue May 13, 2020
…ests will get the result when they arrive, if one is set. A new result can also be provided at any time, but it will only affect requests arriving after it has been set, not the previous. Closes #78
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request input needed When an issue requires input or suggestions question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants