c# - 在 headless 单元测试中运行时,如何使 WebBrowser 完成导航?

标签 c# wpf nunit webbrowser-control

我们有一个 WPF 应用程序在 <WebBrowser/> 中加载一些内容控制,然后根据加载的内容进行一些调用。使用正确的模拟,我们认为我们可以在无显示单元测试(在本例中为 NUnit)中对此进行测试。但是 WebBrowser控件不想很好地播放。

问题是我们从未收到 LoadCompletedNavigated事件。显然这是因为网页在实际呈现之前永远不会“加载”(see this MSDN thread) .我们确实收到了 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 A.R.E.");
        autoResetEvent.WaitOne();
        // TEST HANGS BEFORE REACHING HERE.
        Console.WriteLine("Got it!");
    }
}

最佳答案

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

这是一个如何做到这一点的例子:

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();
    }
}

用法:

// 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反而。我发布了一个类似的 self-contained example从控制台应用程序执行此操作。

关于c# - 在 headless 单元测试中运行时,如何使 WebBrowser 完成导航?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21288489/

相关文章:

c# - 禁用 ASP :dropdownlist based on the selection of another ASP:Dropdownlist. 值

c# - 文件上传读取到内存并用作文本文件 - 有更好的方法吗?

c# - 如何在 WPF 中以编程方式设置 clr 命名空间属性

c# - 如何使用 NUnit Test 对 uwp 应用程序进行单元测试

c# - 我们可以在 asp.net 中对 global.asax 进行单元测试吗?

c# - 显示 FolderbrowserDialog 后不会触发某些事件

c# - Xamarin.Forms 条目 - 自定义行为和 MVVM

wpf - 如何在不使用扩展器的情况下将 WPF 工具栏绑定(bind)到我的 VM 中的集合

wpf - 处理 WPF 应用程序中所有异常的最佳实践是什么?

visual-studio - 由于 FileNotFoundException、异常转换测试,NUnit VS Adapter 无法发现测试