adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
588 stars 49 forks source link

Is it possible to use TemplateHost with a ShellPage? #216

Closed powerdude closed 7 months ago

powerdude commented 7 months ago

Describe the bug Ive created a unit test for a ShellPage and want to Tap on the shell content so that I can test that it shows another page. There doesn't appear to be a SendClicked, or something similar on ShellContent to trigger the navigation.

class MainPage : Component
{
   public override VisualNode Render() 
    => new Shell
    {
        new FlyoutItem("Page1")
        {
            new ShellContent()
                .AutomationId("page1")
                .RenderContent(() => new ContentPage("Page1"))
        },
        new FlyoutItem("Page2")
        {
                new ShellContent()
                .AutomationId("page2")
                .RenderContent(() => new ContentPage("Page2")
                {
                    new Button("Goto to Page1")
                        .HCenter()
                        .VCenter()
                    .OnClicked(async ()=> await MauiControls.Shell.Current.GoToAsync("//page-1"))
                })
        }
    }
    .ItemTemplate(RenderItemTemplate);

    static VisualNode RenderItemTemplate(MauiControls.BaseShellItem item)
        => new Grid("68", "*")
        {
            new Label(item.Title)
                .VCenter()
                .Margin(10,0)
        };
}

To Reproduce

public class ShellPageTest
{
[Fact] public void CheckNav()
{
  using var navigationContainer = new NavigationContainer();
   var shell = TemplateHost.Create(new ShellPage());
shell.Find<ShellContent>("page1")... // what action can trigger the click

var childPage = navigationContainer.AttachHost();  // does this even work with shell.  
   childPage.Title.Should().Be("Page 1");
}
}

Expected behavior a way to trigger the click on the shell content and a way to check the page that was navigated to.

adospace commented 7 months ago

Hi, I've fixed the issue in the ShellContent implementation that causes the automation system to fail to find the button inside the RenderContent callback.

NOTE: Maui shell is somewhat tricky to test. The main problem is that when testing the Dispatcher and the Shell. Current objects aren't available. I'm thinking of a better way to manage it, but in the meaning, you have to grab a reference to the shell and call GotoAsync on that object to actually navigate in a test.

Updating to the latest version 2.0.18 and using code like the following you should be able to test your code:

    private MauiControls.Shell _shellRef;

    public override VisualNode Render()
        => new Shell(shellRef => _shellRef = shellRef)
        {
            new FlyoutItem("Page1")
            {
                new ShellContent()                
                    .RenderContent(() => new ContentPage("Page1"))
                    .Route("page-1")
            }
            .AutomationId("FlyoutItem_Page1"),

            new FlyoutItem("Page2")
            {
                new ShellContent()                
                    .RenderContent(() => new ContentPage("Page2")
                    {
                        new Button("Goto to Page1")
                            .HCenter()
                            .VCenter()
                            .AutomationId("GotoPage1Button")
                        .OnClicked(async ()=> await _shellRef.GoToAsync("//page-1"))
                    })
                    .Route("page-2")
            }
            .AutomationId("FlyoutItem_Page2")
        }
        .AutomationId("MainShell")
        .ItemTemplate(RenderItemTemplate);

    static VisualNode RenderItemTemplate(MauiControls.BaseShellItem item)
        => new Grid("68", "*")
        {
            new Label(item.Title)
                .VCenter()
                .Margin(10,0)
        };
 var mainPageNode = TemplateHost.Create(new MainPageIssue218());

            var shell = mainPageNode.Find<MauiControls.Shell>("MainShell");

            var shellItem1 = shell.Find<MauiControls.FlyoutItem>("FlyoutItem_Page1");
            var shellItem2 = shell.Find<MauiControls.FlyoutItem>("FlyoutItem_Page2");

            shell.CurrentItem.ShouldBe(shellItem1);

            shell.CurrentItem = shellItem2;

            shell.CurrentItem.ShouldBe(shellItem2);

            var gotoPage1Button = mainPageNode.Find<MauiControls.Button>("GotoPage1Button");
            gotoPage1Button.SendClicked();

            shell.CurrentItem.ShouldBe(shellItem1);

I'm working to add more samples that show how to test components that will be published in the samples repository: https://github.com/adospace/mauireactor-samples/tree/main/Controls/SampleTests

powerdude commented 7 months ago

thanks for the update. My sample I posted above wasn't my full use case. I have a FlyoutItem that actually has multiple ShellContent in them which is why I placed an AutomationId on the ShellContent. So, it would still be helpful to try to test a FlyoutItem with multiple content in them.

     new Shell 
     {
            new FlyoutItem("Page1")
            {
                new ShellContent()                
                    .RenderContent(() => new ContentPage("Page1a"))
                    .Route("page-1a"),
                new ShellContent()                
                    .RenderContent(() => new ContentPage("Page1b"))
                    .Route("page-1b"),
            }
            .AutomationId("FlyoutItem_Page1")
            .FlyoutDisplayOptions(FlyoutDisplayOptions.AsMultipleItems),
            new ShellContent("Page2")
                    .RenderContent(() => new ContentPage("Page2"))
                    .Route("page-2"),
     }
adospace commented 7 months ago

added a sample test here: https://github.com/adospace/mauireactor-samples/tree/main/Controls/SampleTests

   public override VisualNode Render()
        => Shell(
            FlyoutItem(
                Tab(
                    ShellContent("Home")
                        .Icon("home.png")
                        .RenderContent(()=> new HomePage())
                        .AutomationId("home_item"),

                    ShellContent("Comments")
                        .Icon("comments.png")
                        .RenderContent(()=> new CommentsPage())
                        .AutomationId("comments_item")
                )
                .Title("Notifications")
                .Icon("bell.png")
                .AutomationId("tab"),

                ShellContent("Database")
                    .Icon("database.png")
                    .RenderContent(()=> new DatabasePage())
                    .AutomationId("database_item"),

                ShellContent("Notifications")
                    .Icon("bell.png")
                    .RenderContent(()=> new NotificationsPage())
                    .AutomationId("notifications_item")
            )
            .AutomationId("flyout_item")
            .FlyoutDisplayOptions(MauiControls.FlyoutDisplayOptions.AsMultipleItems),

            ShellContent("Settings")
                .Icon("gear.png")
                .RenderContent(()=> new SettingsPage())
                .AutomationId("settings_item")
        )
        .AutomationId("MainShell");
        [Test]
        public void TestNavigationMainPage2()
        {
            var mainPageNode = TemplateHost.Create(new MainPage2());

            var shell = mainPageNode.Find<MauiControls.Shell>("MainShell");
            var tab = shell.Find<MauiControls.Tab>("tab");

            var notifications_item = shell.Find<MauiControls.ShellContent>("notifications_item");
            var home_item = shell.Find<MauiControls.ShellContent>("home_item");
            var settings_item = shell.Find<MauiControls.ShellContent>("settings_item");
            var flyout_item = shell.Find<MauiControls.FlyoutItem>("flyout_item");
            var database_item = shell.Find<MauiControls.ShellContent>("database_item");

            shell.CurrentItem.ShouldBe(flyout_item);

            flyout_item.CurrentItem.ShouldBe(tab);

            tab.CurrentItem.ShouldBe(home_item);

            //Navigate to database page
            flyout_item.CurrentItem = database_item;

            flyout_item.CurrentItem.ShouldBe(database_item);
        }
powerdude commented 7 months ago

Ok, I understand what you've did. The final assertion is testing that CurrentItem was set, but how would you get a reference to DatabasePage so that you could click a button or check the value of a label. shell.CurrentPage is null,

powerdude commented 7 months ago

Nevermind, looks like you have to use mainPageNode which seems a little weird.

powerdude commented 7 months ago

This can be closed if your current expectation is that the test should use mainPageNode to get any child controls that may be on the new page that is created when setting CurrentItem of the shell.

adospace commented 7 months ago

You can search down from the mainpage node as shown above but also from any other node that is placed below the root in the tree. For example you can use the database_item in the sample above to search for a button that is created inside the database page. The same button is returned searching for example starting from flyout_item as well given that flyout_item contains the database_item.

I'm building some more examples and so far what I described is working using the latest version of MauiReactor.

I will close this ticket in a few days when I'll be confident of the resolution of the issue you found.

Btw thanks for having reported it!

powerdude commented 7 months ago

The Find extension method that takes a TimeSpan is only available for ITemplateHost. Can one be added for IElementController?