3

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!");
    }
}
Adrian Ratnapala
  • 5,485
  • 2
  • 29
  • 39

2 Answers2

1

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.

denniebee
  • 501
  • 4
  • 6
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.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thank I will try it out when I get to work. Do you need to actually `.Show()` the host `Window`, or just insert the `Browser` into it? Last time I called `.Show()` from inside a unit test everything exploded. – Adrian Ratnapala Jan 23 '14 at 04:23
  • @AdrianRatnapala, you do need `.Show()` as it physically creates a Windows handle behind the scene. It will flash and hide, because of `hostWindow.Visibility = Visibility.Hidden`. I can't be sure about NUnit, but this code works for me in the console app (tested as as `Console.WriteLine(RunWpfWebBrowserAsync("http://www.example.com").Result)` from `Main`). – noseratio Jan 23 '14 at 04:27
  • It works. And yes, it flashes the window. Annoying, but such is life. – Adrian Ratnapala Jan 23 '14 at 06:56