如何在无头单元测试运行时,使web浏览器完成导航? [英] How to make WebBrowser complete navigation when running in a headless unit test?
问题描述
我们有一个WPF应用程序在< WebBrowser />
控制中加载一些内容,然后根据加载的内容进行一些调用。使用正确的模拟,我们认为我们可以在一个无显示单元测试(在这种情况下为NUnit)中测试。但是 WebBrowser
控件不想播放得很好。
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.
问题是我们从不收到 LoadCompleted
或导航
事件。显然,这是因为网页从来没有加载,直到实际呈现(请参阅此MSDN线程)。我们确实收到导航
事件,但是对于我们的目的来说太早了。
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.
使 WebBrowser
控制完全工作,即使没有输出显示到?
So is there a way to make the WebBrowser
control work "fully" even when it has no output to display to?
测试用例的剪切版本:
[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!");
}
}
推荐答案
需要从具有消息循环的STA线程分离。您将在该线程上创建一个 WebBrowser
的实例,并抑制脚本错误。注意,WPF WebBrowser
对象需要一个活动主机窗口才能运行。这是它不同于WinForms WebBrowser
。
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: http://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();
}
}
用法:
// blocking call
string content = RunWpfWebBrowserAsync("http://www.example.com").Result;
// async call
string content = await RunWpfWebBrowserAsync("http://www.example.org")
您还可以尝试直接在 NUnit
threadStart >线程,而不实际创建一个新的线程。这样,NUnit线程将运行 Dispatcher
消息循环。我不太熟悉NUnit,足以预测它是否可以工作。
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.
如果你不想创建一个主机窗口,请考虑使用 WinForms WebBrowser
。我发布了类似的自包含示例,可通过控制台应用执行此操作。
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.
这篇关于如何在无头单元测试运行时,使web浏览器完成导航?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!