我们有一个 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")

您也可以尝试直接在 threadStart 线程上运行 NUnit lambda,而无需实际创建新线程。这样,NUnit 线程将运行 Dispatcher 消息循环。我对 NUnit 不太熟悉,无法预测它是否有效。

如果您不想创建主机窗口,请考虑改用 WinForms WebBrowser 。我从控制台应用程序发布了一个类似的 self-contained example

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

10-15 02:33