NSubstitute icon indicating copy to clipboard operation
NSubstitute copied to clipboard

Get & set callback action result in extension method => dialogService.ShowDialog

Open LeoJHarris opened this issue 2 years ago • 3 comments

Hi all!

Trying to mock an extension method that uses the the Prism Library DialogService with a callback action<IDialogResult>:

The NSub I attempted looked like this:

_subDialogService.When(x => x.ShowDialog(Arg.Is<string>, Arg.Is<IDialogParameters>, Arg.Invoke<Action<IDialogResult>>())).Callback<string, IDialogParameters, Action<IDialogResult>>((n, p, c) => c(new DialogResult(ButtonResult.OK)));

The following is the actual extension method being called in the application but it makes a call to the ShowDialog hence I am calling that in the above:

 public static Task<DialogButtonResult> DisplayPromptDialogAsync(this IDialogService dialogService, string message, string? title = null, string? okButtonText = null, FontImageSource? fontImageSource = null)
        {
            TaskCompletionSource<DialogButtonResult> tcs = new();

            try
            {
                dialogService.ShowDialog(PageNames.GenericPromptDialog, new DialogParameters
                {
                    { NavigationParameterKeys.Title, title },
                    { NavigationParameterKeys.Message, message },
                    { NavigationParameterKeys.OkButtonText, okButtonText },
                    { NavigationParameterKeys.FontImageSource, fontImageSource }
                }, (result) => tcs.SetResult(result.Parameters.GetValue<DialogButtonResult>("result")));
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }

            return tcs.Task;
        }

The Prism Library ShowDialog being called:

public interface IDialogService
    {
        void ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);
    }

Finally the Logout functionality I need to test against is this which contains the extension method:

private async Task logoutAsync()
        {
            DialogButtonResult dialogButtonResult = await _dialogService.DisplayPromptDialogAsync(message: LocalizationResourceManager.Current.GetValue("SignOutMessage"), okButtonText: LocalizationResourceManager.Current.GetValue("SignOutText"), fontImageSource: new FontImageSource
            {
                Glyph = MaterialDesignIcons.Logout,
                FontFamily = nameof(MaterialDesignIcons),
                Color = Color.White,
                Size = (OnPlatform<double>)PrismApplicationBase.Current.Resources["FontBodySize"]
            }).ConfigureAwait(true);

            if (dialogButtonResult is DialogButtonResult.OK && await _deepLinkingService.GoToLoginPageAsync(SignoutCustomerActionType.Complete).ConfigureAwait(false))
            {
                _eventAggregator.GetEvent<ApplicationInactivityTimerChangedEventArgs>().Publish(ApplicationInactivityTimerStatus.Stop);
            }
        }

Bassically I need to be able to unit test this method above by returning DialogButtonResult.OK and then I will do an assert like the following:

// Assert
            _subEventAggregator.Received().Received().GetEvent<ApplicationInactivityTimerChangedEventArgs>().Publish(ApplicationInactivityTimerStatus.Stop);

An equivalent in MOQ I found was something like this but I have been struggling to translate this to NSubstitute (https://stackoverflow.com/questions/64770095/testing-prism-dialogservice):

// Arrange
var dialogServiceMock = new Mock<IDialogService>();
dialogServiceMock.Setup( x => x.ShowDialog( It.IsAny<string>(), It.IsAny<IDialogParameters>(), It.IsAny<Action<IDialogResult>>() ) )
                 .Callback<string, IDialogParameters, Action<IDialogResult>>( ( n, p, c ) => c( new DialogResult( ButtonResult.OK ) ) );

Currently giving an error on the Action<IDialogResult>

image

LeoJHarris avatar May 19 '23 00:05 LeoJHarris

You expect that everyone can compile your code. It would be much easier to help, if you could provide a sample code.

If all you want is a substitute for IDialogService to call the action with your desired DialogResult,, you just have to write:

var dialogService = Substitute.For<IDialogService>();
dialogService.ShowDialog(Arg.Any<string>(), Arg.Any<IDialogParameters>(), Arg.Invoke<IDialogResult>(new DialogResult(ButtonResult.OK)));

Then any call like this, will have the desired result:

ButtonResult result = ButtonResult.None;
dialogService.ShowDialog("Test", null, r => result = r.Result);
result.Should().Be(ButtonResult.OK);

GeraldLx avatar May 19 '23 08:05 GeraldLx

@GeraldLx I think I have discovered a problem, DialogResult was previously marked public but in the source code now it appears internal so not sure if I can proceed with this anymore unfortunately, looks like it had changed after that example I found using MOQ which gave me the impression it was possible but no way to call new DialogResult(DialogButtonResult.OK)

test1121

LeoJHarris avatar May 21 '23 22:05 LeoJHarris

I still dont know which version you are talking about. I just pulled the latest PRISM NuGet packages.

I also dont understand why the change of DialogResult to internal is a problem. I mean the callback just wants an instance of IDialogResult. Basically that is what NSubstitute is designed for. After all you just want a result of ButonResult.OK. So if it is not public accessable, just create one:

    var dialogService = Substitute.For<IDialogService>();
    dialogService.WhenForAnyArgs(ds => ds.ShowDialog(default, default, default)).Do(c =>
    {
      var callback = c.Arg<Action<IDialogResult>>();
      var dialogResult = Substitute.For<IDialogResult>();
      dialogResult.Result.Returns(ButtonResult.OK);
      
      callback(dialogResult);
    });

Hope that helps.

GeraldLx avatar May 22 '23 22:05 GeraldLx

I assume your question has been answered, if not, please let us know!

304NotModified avatar Apr 29 '24 12:04 304NotModified

@304NotModified timely reminder that this issue did not get resolved unfortunately, checked now and getting the following issue:

image

As you can see from the following the parameters is null, not even sure there is some other underlying issue?

Unfortunately the code @GeraldLx provided did not compile but I made the change as follows:

My test case is the following:

[Fact]
    public async Task ForceReAuthenticateInActiveCustomer_AuthCodeValid_HomeNavigationAsync()
    {
        // Arrange
        IUserManagementService unitUnderTest = createService();

        DialogParameters dialogParameters = new()
        {
            {
                NavigationParameterKeys.AuthenticationEventType, AuthenticationEventType.InActivityTimeout
            }
        };

        _subDialogService.WhenForAnyArgs(dialogService =>
        dialogService.ShowDialog(PageNames.AuthenticationDialog, dialogParameters)).Do(callInfo
        =>
        {
            Action<DialogResult> callback = callInfo.Arg<Action<DialogResult>>();

            DialogResult dialogResult = new()
            {
                Parameters = new DialogParameters
                {
                    { NavigationParameterKeys.AuthenticationActionPerformed,  AuthenticationResponseAction.None }
                }
            };

            callback(dialogResult);
        });

        // Act
        await unitUnderTest.ForceReAuthenticateInActiveCustomerAsync().ConfigureAwait(true);

        // Assert
        _ = _subDeepLinkingService.ReceivedCalls().Should().BeEmpty();
    }
public static Task<AuthenticationResponseAction?> DisplayAuthenticationDialogAsync(this IDialogService dialogService, AuthenticationEventType authenticationEventType, INavigationParameters? navigationParameters = null)
    {
        TaskCompletionSource<AuthenticationResponseAction?> tcs = new();

        try
        {
            DialogParameters dialogParameters = new()
            {
                { NavigationParameterKeys.AuthenticationEventType, authenticationEventType }
            };

            if (navigationParameters is not null)
            {
                foreach (KeyValuePair<string, object> navigationParameter in navigationParameters)
                {
                    dialogParameters.Add(navigationParameter.Key, navigationParameter.Value);
                }
            }

            switch (authenticationEventType)
            {
                case AuthenticationEventType.InActivityTimeout:
                    dialogParameters.Add(KnownDialogParameters.CloseOnBackgroundTapped, false);
                    break;

                case AuthenticationEventType.PreviouslySignedOut:
                case AuthenticationEventType.AuthCodeExpired:
                    dialogParameters.Add(KnownDialogParameters.CloseOnBackgroundTapped, true);
                    break;
            }

            dialogService.ShowDialog(PageNames.AuthenticationDialog, dialogParameters, (result) => tcs.SetResult(result.Parameters.GetValue<AuthenticationResponseAction?>(NavigationParameterKeys.AuthenticationActionPerformed)));
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }

        return tcs.Task;
    }

AuthenticationEventType has a value InActivityTimeout which if returned from the dialog I want to make sure none of the deeplinking methods are called. I apologize but providing any sample on this is not practical.

Also Prism DialogResult has Parameters get only so I have implemented my own class to put the setter on it as per this link so I can retrieve the parameters later.

public class DialogResult : IDialogResult
{
    public Exception Exception => throw new NotImplementedException();
    public IDialogParameters Parameters { get; set; }
}

LeoJHarris avatar May 08 '24 06:05 LeoJHarris

Happy to close this now as I got this working!

Updated how the DialogService is resolved:

switch (await App.ContainerProvider.Resolve<IDialogService>()
.DisplayAuthenticationDialogAsync(AuthenticationEventType.InActivityTimeout)
.ConfigureAwait(true))

To this:

switch (await _dialogService.DisplayAuthenticationDialogAsync(AuthenticationEventType.InActivityTimeout)
.ConfigureAwait(true))

LeoJHarris avatar May 08 '24 23:05 LeoJHarris