问题
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