如何在无头unit testing中运行时使WebBrowser完成导航?

我们有一个WPF应用程序,它在控件中加载一些内容,然后根据加载的内容进行一些调用。 使用正确的模拟,我们认为我们可以在无显示unit testing(在这种情况下为NUnit)内测试。 但是WebBrowser控件不想玩得很好。

问题是我们从未收到LoadCompletedNavigated事件。 显然这是因为在实际呈现之前,网页永远不会“加载” (请参阅此MSDN线程) 。 我们确实收到了Navigating活动,但这对我们来说太早了。

那么有没有办法使WebBrowser控件“完全”工作,即使它没有输出显示?

以下是测试用例的简化版本:

 [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 ARE"); autoResetEvent.WaitOne(); // TEST HANGS BEFORE REACHING HERE. Console.WriteLine("Got it!"); } } 

您需要使用消息循环分离STA线程。 您将在该线程上创建WebBrowser实例并禁止脚本错误。 注意,WPF WebBrowser对象需要一个实时主机窗口才能运行。 这就是它与WinForms WebBrowser的不同之处。

以下是如何完成此操作的示例:

 static async Task RunWpfWebBrowserAsync(string url) { // return the result via Task var resultTcs = new TaskCompletionSource(); // 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(); 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, ie: // "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 lambda,而不实际创建新线程。 这样,NUnit线程将运行Dispatcher消息循环。 我对NUnit不熟悉,不足以预测它是否有效。

如果您不想创建主机窗口,请考虑使用WinForms WebBrowser 。 我在控制台应用程序中发布了一个类似的自包含示例 。

不确定它是否有效,但是如果你使用像Moq这样的模拟框架,你可以将“IsLoaded”属性模拟为’true’并欺骗WebBrowser加载。

当然,这可能会发现WebBrowser实际上需要一个完全可用的显示器,这不会让我感到惊讶。 很多html,javascript和dom都取决于屏幕测量和-events。