How to make WebBrowser complete navigation when running in a headless unit test?

折月煮酒 提交于 2019-12-12 15:34:52

问题


We have a WPF application that loads some content in a <WebBrowser/> control and then makes some calls based on what was loaded. With the right mocks, we think we can test this inside a displayless unit test (NUnit in this case). But the WebBrowser control doesn't want to play nicely.

The problem is that we never receive the LoadCompleted or Navigated events. Apparently this is because a web-page is never "Loaded" until it is actually rendered (see this MSDN thread). We do receive the Navigating event, but that comes far too early for our purposes.

So is there a way to make the WebBrowser control work "fully" even when it has no output to display to?

Here is a cut-down version of the test case:

[TestFixture, RequiresSTA]
class TestIsoView
{
    [Test] public void PageLoadsAtAll()
    {
        Console.WriteLine("I'm a lumberjack and I'm OK");
        WebBrowser wb = new WebBrowser();

        // An AutoResetEvent allows us to synchronously wait for an event to occur.
        AutoResetEvent autoResetEvent = new AutoResetEvent(false);
        //wb.LoadCompleted += delegate  // LoadCompleted is never received
        wb.Navigated += delegate  // Navigated is never received
        //wb.Navigating += delegate // Navigating *is* received
        {
            // We never get here unless we wait on wb.Navigating
            Console.WriteLine("The document loaded!!");
            autoResetEvent.Set();
        };

        Console.WriteLine("Registered signal handler", "Navigating");

        wb.NavigateToString("Here be dramas");
        Console.WriteLine("Asyncronous Navigations started!  Waiting for A.R.E.");
        autoResetEvent.WaitOne();
        // TEST HANGS BEFORE REACHING HERE.
        Console.WriteLine("Got it!");
    }
}

回答1:


You'd need to spin off an STA thread with a message loop for that. You'd create an instance of WebBrowser on that thread and suppress script errors. Note, a WPF WebBrowser object needs a live host window to function. That's how it's different from WinForms WebBrowser.

Here is an example of how this can be done:

static async Task<string> RunWpfWebBrowserAsync(string url)
{
    // return the result via Task
    var resultTcs = new TaskCompletionSource<string>();

    // the main WPF WebBrowser driving logic
    // to be executed on an STA thread
    Action startup = async () => 
    {
        try
        {
            // create host window
            var hostWindow = new Window();
            hostWindow.ShowActivated = false;
            hostWindow.ShowInTaskbar = false;
            hostWindow.Visibility = Visibility.Hidden;
            hostWindow.Show();

            // create a WPF WebBrowser instance
            var wb = new WebBrowser();
            hostWindow.Content = wb;

            // suppress script errors: https://stackoverflow.com/a/18289217
            // touching wb.Document makes sure the underlying ActiveX has been created
            dynamic document = wb.Document; 
            dynamic activeX = wb.GetType().InvokeMember("ActiveXInstance",
                BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                null, wb, new object [] { });
            activeX.Silent = true;

            // navigate and handle LoadCompleted
            var navigationTcs = new TaskCompletionSource<bool>();
            wb.LoadCompleted += (s, e) => 
                navigationTcs.TrySetResult(true);
            wb.Navigate(url);
            await navigationTcs.Task;

            // do the WebBrowser automation
            document = wb.Document;
            // ...

            // return the content (for example)
            string content = document.body.outerHTML;
            resultTcs.SetResult(content);
        }
        catch (Exception ex)
        {
            // propogate exceptions to the caller of RunWpfWebBrowserAsync
            resultTcs.SetException(ex);
        }

        // end the tread: the message loop inside Dispatcher.Run() will exit
        Dispatcher.ExitAllFrames();
    };

    // thread procedure
    ThreadStart threadStart = () =>
    {
        // post the startup callback
        // it will be invoked when the message loop pumps
        Dispatcher.CurrentDispatcher.BeginInvoke(startup);
        // run the WPF Dispatcher message loop
        Dispatcher.Run();
        Debug.Assert(true);
    };

    // start and run the STA thread
    var thread = new Thread(threadStart);
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    try
    {
        // use Task.ConfigureAwait(false) to avoid deadlock on a UI thread
        // if the caller does a blocking call, i.e.:
        // "RunWpfWebBrowserAsync(url).Wait()" or 
        // "RunWpfWebBrowserAsync(url).Result"
        return await resultTcs.Task.ConfigureAwait(false);
    }
    finally
    {
        // make sure the thread has fully come to an end
        thread.Join();
    }
}

Usage:

// blocking call
string content = RunWpfWebBrowserAsync("http://www.example.com").Result;

// async call
string content = await RunWpfWebBrowserAsync("http://www.example.org")

You may also try to run threadStart lambda directly on your NUnit thread, without actually creating a new thread. This way, the NUnit thread will run the Dispatcher message loop. I'm not familiar with NUnit well enough to predict if that works.

If you don't want to create a host window, consider using WinForms WebBrowser instead. I posted a similar self-contained example of doing that from a console app.




回答2:


Not sure if it works, but if you are using a mocking framework like Moq, you could mock the "IsLoaded" property to being 'true' and trick the WebBrowser in being loaded.

This of course, might reveal that the WebBrowser actually needs a display to be completely functional, which wouldn't surprise me. Much of html, javascript and the dom depends on screen-measurements and -events.



来源:https://stackoverflow.com/questions/21288489/how-to-make-webbrowser-complete-navigation-when-running-in-a-headless-unit-test

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!