Skip to content

Commit 98cb4cf

Browse files
committed
Enhancement - Add WaitForNavigationAsync
- Add new WaitForNavigationAsync method - Change LoadUrlAsync to return CefErrorCode.Failed for HttpStatusCode = -1 (better represents there was an error) Example: var navigationTask = browser.WaitForNavigationAsync(); var evaluateTask = browser.EvaluateScriptAsync($"window.location.href = '{expected}';"); await Task.WhenAll(navigationTask, evaluateTask); var navigationResponse = navigationTask.Result; Resolves #4073
1 parent 93e5161 commit 98cb4cf

File tree

8 files changed

+347
-2
lines changed

8 files changed

+347
-2
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using Xunit.Abstractions;
2+
using Xunit;
3+
using System.Threading.Tasks;
4+
using CefSharp.OffScreen;
5+
using CefSharp.Example;
6+
using Nito.AsyncEx;
7+
using System;
8+
using System.Threading;
9+
10+
namespace CefSharp.Test.Navigation
11+
{
12+
//NOTE: All Test classes must be part of this collection as it manages the Cef Initialize/Shutdown lifecycle
13+
[Collection(CefSharpFixtureCollection.Key)]
14+
public class WaitForNavigationAsyncTests
15+
{
16+
17+
private readonly ITestOutputHelper output;
18+
private readonly CefSharpFixture fixture;
19+
20+
public WaitForNavigationAsyncTests(ITestOutputHelper output, CefSharpFixture fixture)
21+
{
22+
this.fixture = fixture;
23+
this.output = output;
24+
}
25+
26+
[Fact]
27+
public async Task CanWork()
28+
{
29+
const string expected = CefExample.HelloWorldUrl;
30+
31+
using (var browser = new ChromiumWebBrowser(CefExample.DefaultUrl))
32+
{
33+
var response = await browser.WaitForInitialLoadAsync();
34+
35+
Assert.True(response.Success);
36+
37+
var navigationTask = browser.WaitForNavigationAsync();
38+
var evaluateTask = browser.EvaluateScriptAsync($"window.location.href = '{expected}';");
39+
40+
await Task.WhenAll(navigationTask, evaluateTask);
41+
42+
var navigationResponse = navigationTask.Result;
43+
44+
var mainFrame = browser.GetMainFrame();
45+
Assert.True(mainFrame.IsValid);
46+
Assert.Equal(expected, mainFrame.Url);
47+
Assert.Equal(200, navigationResponse.HttpStatusCode);
48+
49+
output.WriteLine("Url {0}", mainFrame.Url);
50+
}
51+
}
52+
53+
[Fact]
54+
public async Task CanWaitForInvalidDomain()
55+
{
56+
const string expected = "https://notfound.cefsharp.test";
57+
using (var browser = new ChromiumWebBrowser(CefExample.DefaultUrl))
58+
{
59+
var response = await browser.WaitForInitialLoadAsync();
60+
61+
Assert.True(response.Success);
62+
63+
var navigationTask = browser.WaitForNavigationAsync();
64+
var evaluateTask = browser.EvaluateScriptAsync($"window.location.href = '{expected}';");
65+
66+
await Task.WhenAll(navigationTask, evaluateTask);
67+
68+
var navigationResponse = navigationTask.Result;
69+
70+
var mainFrame = browser.GetMainFrame();
71+
Assert.True(mainFrame.IsValid);
72+
Assert.False(navigationResponse.Success);
73+
Assert.Contains(expected, mainFrame.Url);
74+
Assert.Equal(CefErrorCode.NameNotResolved, navigationResponse.ErrorCode);
75+
76+
output.WriteLine("Url {0}", mainFrame.Url);
77+
}
78+
}
79+
80+
[Fact]
81+
public async Task CanTimeout()
82+
{
83+
const string expected = "The operation has timed out.";
84+
85+
using (var browser = new ChromiumWebBrowser(CefExample.DefaultUrl))
86+
{
87+
var response = await browser.WaitForInitialLoadAsync();
88+
89+
Assert.True(response.Success);
90+
91+
var exception = await Assert.ThrowsAnyAsync<TimeoutException>(async () =>
92+
{
93+
await browser.WaitForNavigationAsync(timeout:TimeSpan.FromMilliseconds(100));
94+
});
95+
96+
Assert.Contains(expected, exception.Message);
97+
98+
output.WriteLine("Exception {0}", exception.Message);
99+
}
100+
}
101+
102+
[Fact]
103+
public async Task CanCancel()
104+
{
105+
const string expected = "A task was canceled.";
106+
107+
using (var browser = new ChromiumWebBrowser(CefExample.DefaultUrl))
108+
{
109+
var response = await browser.WaitForInitialLoadAsync();
110+
111+
var cancellationTokenSource = new CancellationTokenSource();
112+
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(1));
113+
114+
Assert.True(response.Success);
115+
116+
var exception = await Assert.ThrowsAnyAsync<TaskCanceledException>(async () =>
117+
{
118+
await browser.WaitForNavigationAsync(cancellationToken: cancellationTokenSource.Token);
119+
});
120+
121+
Assert.Contains(expected, exception.Message);
122+
123+
output.WriteLine("Exception {0}", exception.Message);
124+
}
125+
}
126+
}
127+
}

CefSharp.WinForms/Host/ChromiumHostControl.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.ComponentModel;
77
using System.Drawing;
8+
using System.Threading;
89
using System.Threading.Tasks;
910
using System.Windows.Forms;
1011

@@ -249,6 +250,13 @@ public Task<LoadUrlAsyncResponse> LoadUrlAsync(string url)
249250
return CefSharp.WebBrowserExtensions.LoadUrlAsync(this, url);
250251
}
251252

253+
/// <inheritdoc/>
254+
public Task<WaitForNavigationAsyncResponse> WaitForNavigationAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
255+
{
256+
//WaitForNavigationAsync is actually a static method so that CefSharp.Wpf.HwndHost can reuse the code
257+
return CefSharp.WebBrowserExtensions.WaitForNavigationAsync(this, timeout, cancellationToken);
258+
}
259+
252260
/// <summary>
253261
/// Returns the main (top-level) frame for the browser window.
254262
/// </summary>

CefSharp/IChromiumWebBrowserBase.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
44

55
using System;
6+
using System.Threading;
67
using System.Threading.Tasks;
78

89
namespace CefSharp
@@ -95,6 +96,33 @@ public interface IChromiumWebBrowserBase : IDisposable
9596
/// </returns>
9697
Task<LoadUrlAsyncResponse> LoadUrlAsync(string url);
9798

99+
/// <summary>
100+
/// This resolves when the browser navigates to a new URL or reloads.
101+
/// It is useful for when you run code which will indirectly cause the browser to navigate.
102+
/// A common use case would be when executing javascript that results in a navigation. e.g. clicks a link
103+
/// This must be called before executing the action that navigates the browser. It may not resolve correctly
104+
/// if called after.
105+
/// </summary>
106+
/// <remarks>
107+
/// Usage of the <c>History API</c> <see href="https://developer.mozilla.org/en-US/docs/Web/API/History_API"/> to change the URL is considered a navigation
108+
/// </remarks>
109+
/// <param name="timeout">optional timeout, if not specified defaults to five(5) seconds.</param>
110+
/// <param name="cancellationToken">optional CancellationToken</param>
111+
/// <returns>Task which resolves when <see cref="IChromiumWebBrowserBase.LoadingStateChanged"/> has been called with <see cref="LoadingStateChangedEventArgs.IsLoading"/> false.
112+
/// or when <see cref="IChromiumWebBrowserBase.LoadError"/> is called to signify a load failure.
113+
/// </returns>
114+
/// <example>
115+
/// <code>
116+
/// <![CDATA[
117+
/// string script = "document.getElementsByTagName('a')[0].click();";
118+
/// await Task.WhenAll(
119+
/// chromiumWebBrowser.WaitForNavigationAsync(),
120+
/// chromiumWebBrowser.EvaluateScriptAsync(jsScript3));
121+
/// ]]>
122+
/// </code>
123+
/// </example>
124+
Task<WaitForNavigationAsyncResponse> WaitForNavigationAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default);
125+
98126
/// <summary>
99127
/// A flag that indicates whether the WebBrowser is initialized (true) or not (false).
100128
/// </summary>

CefSharp/Internals/Partial/ChromiumWebBrowser.Partial.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,13 @@ public Task<LoadUrlAsyncResponse> LoadUrlAsync(string url)
369369
return CefSharp.WebBrowserExtensions.LoadUrlAsync(this, url);
370370
}
371371

372+
/// <inheritdoc/>
373+
public Task<WaitForNavigationAsyncResponse> WaitForNavigationAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
374+
{
375+
//WaitForNavigationAsync is actually a static method so that CefSharp.Wpf.HwndHost can reuse the code
376+
return CefSharp.WebBrowserExtensions.WaitForNavigationAsync(this, timeout, cancellationToken);
377+
}
378+
372379
/// <inheritdoc/>
373380
public Task<LoadUrlAsyncResponse> WaitForInitialLoadAsync()
374381
{
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// https://github.com/dotnet/runtime/blob/933988c35c172068652162adf6f20477231f815e/src/libraries/Common/tests/System/Threading/Tasks/TaskTimeoutExtensions.cs#L1
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// https://github.com/dotnet/runtime/blob/933988c35c172068652162adf6f20477231f815e/src/libraries/Common/tests/System/Threading/Tasks/TaskTimeoutExtensions.cs#L12
5+
6+
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace CefSharp.Internals
11+
{
12+
/// <summary>
13+
/// WaitAsync polyfills imported from .Net Runtime
14+
/// as we don't get access to this method in older .net versions
15+
/// </summary>
16+
internal static class TaskTimeoutExtensions
17+
{
18+
public static Task<TResult> WaitAsync<TResult>(Task<TResult> task, int millisecondsTimeout) =>
19+
WaitAsync(task, TimeSpan.FromMilliseconds(millisecondsTimeout), default);
20+
21+
public static Task<TResult> WaitAsync<TResult>(Task<TResult> task, TimeSpan timeout) =>
22+
WaitAsync(task, timeout, default);
23+
24+
public static Task<TResult> WaitAsync<TResult>(Task<TResult> task, CancellationToken cancellationToken) =>
25+
WaitAsync(task, Timeout.InfiniteTimeSpan, cancellationToken);
26+
27+
public static async Task<TResult> WaitAsync<TResult>(Task<TResult> task, TimeSpan timeout, CancellationToken cancellationToken)
28+
{
29+
var tcs = new TaskCompletionSource<TResult>();
30+
using (new Timer(s => ((TaskCompletionSource<TResult>)s).TrySetException(new TimeoutException()), tcs, timeout, Timeout.InfiniteTimeSpan))
31+
using (cancellationToken.Register(s => ((TaskCompletionSource<TResult>)s).TrySetCanceled(), tcs))
32+
{
33+
return await (await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)).ConfigureAwait(false);
34+
}
35+
}
36+
}
37+
}

CefSharp/LoadUrlAsyncResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace CefSharp
66
{
77
/// <summary>
8-
/// Response returned from <see cref="IWebBrowser.LoadUrlAsync(string, System.Threading.SynchronizationContext)"/>
8+
/// Response returned from <see cref="IChromiumWebBrowserBase.LoadUrlAsync(string)"/>
99
/// </summary>
1010
public class LoadUrlAsyncResponse
1111
{
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright © 2022 The CefSharp Authors. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
4+
5+
namespace CefSharp
6+
{
7+
/// <summary>
8+
/// WaitForNavigationAsyncResponse
9+
/// </summary>
10+
public class WaitForNavigationAsyncResponse
11+
{
12+
/// <summary>
13+
/// Error Code. If the network request was made successfully this value will be <see cref="CefErrorCode.None"/>
14+
/// (no error occured)
15+
/// </summary>
16+
public CefErrorCode ErrorCode { get; private set; }
17+
18+
/// <summary>
19+
/// Http Status Code. If <see cref="ErrorCode"/> is not equal to <see cref="CefErrorCode.None"/>
20+
/// then this value will be -1.
21+
/// </summary>
22+
public int HttpStatusCode { get; private set; }
23+
24+
/// <summary>
25+
/// If <see cref="ErrorCode"/> is equal to <see cref="CefErrorCode.None"/> and
26+
/// <see cref="HttpStatusCode"/> is equal to 200 (OK) then the main frame loaded without
27+
/// critical error.
28+
/// </summary>
29+
public bool Success
30+
{
31+
get { return ErrorCode == CefErrorCode.None && HttpStatusCode == 200; }
32+
}
33+
34+
/// <summary>
35+
/// Initializes a new instance of the WaitForNavigationAsyncResponse class.
36+
/// </summary>
37+
/// <param name="errorCode">CEF Error Code</param>
38+
/// <param name="httpStatusCode">Http Status Code</param>
39+
public WaitForNavigationAsyncResponse(CefErrorCode errorCode, int httpStatusCode)
40+
{
41+
ErrorCode = errorCode;
42+
HttpStatusCode = httpStatusCode;
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)