Skip to content

Commit e3f9d91

Browse files
bdurranikblok
authored andcommitted
Frame.GoTo and Frame.Navigate
* started work on frame navigation methods. currently the test just times out * implement #674. Page tests are passing. Frame test still hanging. * Update lib/PuppeteerSharp/NetworkManager.cs fix extra line * - remove unused variable after merge conflict - missing frame detached handler * - added test for frame detaching * - rename navigate to navigateasync - rename test to reflect class being tested - added more tests - added more docs * - add test for waitfornaviationasync (second test broken) - some more clean up * add final set of test * - PR feedback * - add missing assert
1 parent ee4224a commit e3f9d91

File tree

10 files changed

+311
-108
lines changed

10 files changed

+311
-108
lines changed

lib/PuppeteerSharp.Tests/FrameTests/ContextTests.cs renamed to lib/PuppeteerSharp.Tests/FrameTests/ExecutionContextTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
namespace PuppeteerSharp.Tests.FrameTests
66
{
77
[Collection("PuppeteerLoaderFixture collection")]
8-
public class ContextTests : PuppeteerPageBaseTest
8+
public class ExecutionContextTests : PuppeteerPageBaseTest
99
{
10-
public ContextTests(ITestOutputHelper output) : base(output)
10+
public ExecutionContextTests(ITestOutputHelper output) : base(output)
1111
{
1212
}
1313

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Net;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Http;
5+
using Xunit;
6+
using Xunit.Abstractions;
7+
8+
namespace PuppeteerSharp.Tests.FrameTests
9+
{
10+
[Collection("PuppeteerLoaderFixture collection")]
11+
public class GoToTests : PuppeteerPageBaseTest
12+
{
13+
public GoToTests(ITestOutputHelper output) : base(output)
14+
{
15+
}
16+
17+
[Fact]
18+
public async Task ShouldNavigateSubFrames()
19+
{
20+
await Page.GoToAsync(TestConstants.ServerUrl + "/frames/one-frame.html");
21+
Assert.Contains("/frames/one-frame.html", Page.Frames[0].Url);
22+
Assert.Contains("/frames/frame.html", Page.Frames[1].Url);
23+
var response = await Page.Frames[1].GoToAsync(TestConstants.EmptyPage);
24+
Assert.Equal(HttpStatusCode.OK, response.Status);
25+
Assert.Same(response.Frame, Page.Frames[1]);
26+
}
27+
28+
[Fact]
29+
public async Task ShouldRejectWhenFrameDetaches()
30+
{
31+
await Page.GoToAsync(TestConstants.ServerUrl + "/frames/one-frame.html");
32+
Server.SetRoute("/empty.html", context => Task.Delay(10000));
33+
var waitForRequestTask = Server.WaitForRequest("/empty.html");
34+
var navigationTask = Page.Frames[1].GoToAsync(TestConstants.EmptyPage);
35+
await waitForRequestTask;
36+
await Page.QuerySelectorAsync("iframe").EvaluateFunctionAsync("frame => frame.remove()");
37+
var exception = await Assert.ThrowsAsync<NavigationException>(async () => await navigationTask);
38+
Assert.Equal("Navigating frame was detached", exception.Message);
39+
}
40+
41+
[Fact]
42+
public async Task ShouldReturnMatchingResponses()
43+
{
44+
// Disable cache: otherwise, chromium will cache similar requests.
45+
await Page.SetCacheEnabledAsync(false);
46+
await Page.GoToAsync(TestConstants.EmptyPage);
47+
// Attach three frames.
48+
var frameTasks = new List<Task<Frame>>
49+
{
50+
FrameUtils.AttachFrameAsync(Page, "frame1", TestConstants.EmptyPage),
51+
FrameUtils.AttachFrameAsync(Page, "frame2", TestConstants.EmptyPage),
52+
FrameUtils.AttachFrameAsync(Page, "frame3", TestConstants.EmptyPage)
53+
};
54+
await Task.WhenAll(frameTasks);
55+
56+
// Navigate all frames to the same URL.
57+
var serverResponses = new List<TaskCompletionSource<string>>();
58+
Server.SetRoute("/one-style.html", async (context) =>
59+
{
60+
var tcs = new TaskCompletionSource<string>();
61+
serverResponses.Add(tcs);
62+
await context.Response.WriteAsync(await tcs.Task);
63+
});
64+
65+
var navigations = new List<Task<Response>>();
66+
for (var i = 0; i < 3; ++i)
67+
{
68+
var waitRequestTask = Server.WaitForRequest("/one-style.html");
69+
navigations.Add(frameTasks[i].Result.GoToAsync(TestConstants.ServerUrl + "/one-style.html"));
70+
await waitRequestTask;
71+
}
72+
// Respond from server out-of-order.
73+
var serverResponseTexts = new string[] { "AAA", "BBB", "CCC" };
74+
for (var i = 0; i < 3; ++i)
75+
{
76+
serverResponses[i].TrySetResult(serverResponseTexts[i]);
77+
var response = await navigations[i];
78+
Assert.Same(frameTasks[i].Result, response.Frame);
79+
Assert.Equal(serverResponseTexts[i], await response.TextAsync());
80+
}
81+
}
82+
}
83+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Net;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
using Xunit.Abstractions;
5+
6+
namespace PuppeteerSharp.Tests.FrameTests
7+
{
8+
[Collection("PuppeteerLoaderFixture collection")]
9+
public class WaitForNavigationTests : PuppeteerPageBaseTest
10+
{
11+
public WaitForNavigationTests(ITestOutputHelper output) : base(output)
12+
{
13+
}
14+
15+
[Fact]
16+
public async Task ShouldWork()
17+
{
18+
await Page.GoToAsync(TestConstants.ServerUrl + "/frames/one-frame.html");
19+
var frame = Page.Frames[1];
20+
var waitForNavigationResult = frame.WaitForNavigationAsync();
21+
22+
await Task.WhenAll(
23+
waitForNavigationResult,
24+
frame.EvaluateFunctionAsync("url => window.location.href = url", TestConstants.ServerUrl + "/grid.html")
25+
);
26+
var response = await waitForNavigationResult;
27+
Assert.Equal(HttpStatusCode.OK, response.Status);
28+
Assert.Contains("grid.html", response.Url);
29+
Assert.Same(frame, response.Frame);
30+
Assert.Contains("/frames/one-frame.html", Page.Url);
31+
}
32+
33+
[Fact]
34+
public async Task ShouldRejectWhenFrameDetaches()
35+
{
36+
await Page.GoToAsync(TestConstants.ServerUrl + "/frames/one-frame.html");
37+
var frame = Page.Frames[1];
38+
Server.SetRoute("/empty.html", context => Task.Delay(10000));
39+
var waitForNavigationResult = frame.WaitForNavigationAsync();
40+
await Task.WhenAll(
41+
Server.WaitForRequest("/empty.html"),
42+
frame.EvaluateFunctionAsync($"() => window.location = '{TestConstants.EmptyPage}'"));
43+
44+
await Page.QuerySelectorAsync("iframe").EvaluateFunctionAsync("frame => frame.remove()");
45+
var response = await waitForNavigationResult;
46+
}
47+
}
48+
}

lib/PuppeteerSharp.Tests/FrameUtils.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
using System;
2-
using System.Text.RegularExpressions;
1+
using System.Text.RegularExpressions;
32
using System.Threading.Tasks;
43

54
namespace PuppeteerSharp.Tests
65
{
76
public static class FrameUtils
87
{
9-
public static async Task AttachFrameAsync(Page page, string frameId, string url)
8+
public static async Task<Frame> AttachFrameAsync(Page page, string frameId, string url)
109
{
11-
await page.EvaluateFunctionAsync(@"(frameId, url) => {
10+
var handle = await page.EvaluateFunctionHandleAsync(@" async (frameId, url) => {
1211
const frame = document.createElement('iframe');
1312
frame.src = url;
1413
frame.id = frameId;
1514
document.body.appendChild(frame);
16-
return new Promise(x => frame.onload = x);
17-
}", frameId, url);
15+
await new Promise(x => frame.onload = x);
16+
return frame
17+
}", frameId, url) as ElementHandle;
18+
return await handle.ContentFrameAsync();
1819
}
1920

2021
public static async Task DetachFrameAsync(Page page, string frameId)

lib/PuppeteerSharp/Frame.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class Frame
4545
internal List<WaitTask> WaitTasks { get; }
4646
internal string Id { get; set; }
4747
internal string LoaderId { get; set; }
48-
internal List<string> LifecycleEvents { get; }
48+
internal List<string> LifecycleEvents { get; }
4949
internal string NavigationURL { get; private set; }
5050

5151
internal Frame(FrameManager frameManager, CDPSession client, Frame parentFrame, string frameId)
@@ -63,7 +63,7 @@ internal Frame(FrameManager frameManager, CDPSession client, Frame parentFrame,
6363
SetDefaultContext(null);
6464

6565
WaitTasks = new List<WaitTask>();
66-
LifecycleEvents = new List<string>();
66+
LifecycleEvents = new List<string>();
6767
}
6868

6969
#region Properties
@@ -98,6 +98,50 @@ internal Frame(FrameManager frameManager, CDPSession client, Frame parentFrame,
9898

9999
#region Public Methods
100100

101+
/// <summary>
102+
/// Navigates to an url
103+
/// </summary>
104+
/// <param name="url">URL to navigate page to. The url should include scheme, e.g. https://.</param>
105+
/// <param name="options">Navigation parameters.</param>
106+
/// <returns>Task which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect.</returns>
107+
/// <seealso cref="GoToAsync(string, int?, WaitUntilNavigation[])"/>
108+
public Task<Response> GoToAsync(string url, NavigationOptions options) => FrameManager.NavigateFrameAsync(this, url, options);
109+
110+
/// <summary>
111+
/// Navigates to an url
112+
/// </summary>
113+
/// <param name="url">URL to navigate page to. The url should include scheme, e.g. https://.</param>
114+
/// <param name="timeout">maximum navigation time in milliseconds. Defaults to 30 seconds. Pass 0
115+
/// to disable timeout. The default value can be changed by using the <see cref="Page.DefaultNavigationTimeout"/>
116+
/// property.</param>
117+
/// <param name="waitUntil">When to consider navigation succeeded, defaults to <see cref="WaitUntilNavigation.Load"/>. Given an array of <see cref="WaitUntilNavigation"/>, navigation is considered to be successful after all events have been fired</param>
118+
/// <returns>Task which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect</returns>
119+
public Task<Response> GoToAsync(string url, int? timeout = null, WaitUntilNavigation[] waitUntil = null)
120+
=> GoToAsync(url, new NavigationOptions { Timeout = timeout, WaitUntil = waitUntil });
121+
122+
/// <summary>
123+
/// This resolves when the frame navigates to a new URL or reloads.
124+
/// It is useful for when you run code which will indirectly cause the frame to navigate.
125+
/// </summary>
126+
/// <param name="options">navigation options</param>
127+
/// <returns>Task which resolves to the main resource response.
128+
/// In case of multiple redirects, the navigation will resolve with the response of the last redirect.
129+
/// In case of navigation to a different anchor or navigation due to History API usage, the navigation will resolve with `null`.
130+
/// </returns>
131+
/// <remarks>
132+
/// 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
133+
/// </remarks>
134+
/// <example>
135+
/// <code>
136+
/// <![CDATA[
137+
/// var navigationTask =frame.page.WaitForNavigationAsync();
138+
/// await frame.ClickAsync("a.my-link");
139+
/// await navigationTask;
140+
/// ]]>
141+
/// </code>
142+
/// </example>
143+
public Task<Response> WaitForNavigationAsync(NavigationOptions options = null) => FrameManager.WaitForFrameNavigationAsync(this, options);
144+
101145
/// <summary>
102146
/// Executes a script in browser context
103147
/// </summary>

lib/PuppeteerSharp/FrameManager.cs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics.Contracts;
4+
using System.Threading.Tasks;
45
using Microsoft.Extensions.Logging;
5-
using Newtonsoft.Json.Linq;
6+
using PuppeteerSharp.Helpers;
67
using PuppeteerSharp.Messaging;
78

89
namespace PuppeteerSharp
@@ -11,21 +12,25 @@ internal class FrameManager
1112
{
1213
private readonly CDPSession _client;
1314
private Dictionary<int, ExecutionContext> _contextIdToContext;
15+
private bool _ensureNewDocumentNavigation;
1416
private readonly ILogger _logger;
17+
private readonly NetworkManager _networkManager;
1518

16-
internal FrameManager(CDPSession client, FrameTree frameTree, Page page)
19+
internal FrameManager(CDPSession client, FrameTree frameTree, Page page, NetworkManager networkManager)
1720
{
1821
_client = client;
1922
Page = page;
2023
Frames = new Dictionary<string, Frame>();
2124
_contextIdToContext = new Dictionary<int, ExecutionContext>();
2225
_logger = _client.Connection.LoggerFactory.CreateLogger<FrameManager>();
26+
_networkManager = networkManager;
2327

2428
_client.MessageReceived += _client_MessageReceived;
2529
HandleFrameTree(frameTree);
2630
}
2731

2832
#region Properties
33+
2934
internal event EventHandler<FrameEventArgs> FrameAttached;
3035
internal event EventHandler<FrameEventArgs> FrameDetached;
3136
internal event EventHandler<FrameEventArgs> FrameNavigated;
@@ -35,6 +40,7 @@ internal FrameManager(CDPSession client, FrameTree frameTree, Page page)
3540
internal Dictionary<string, Frame> Frames { get; set; }
3641
internal Frame MainFrame { get; set; }
3742
internal Page Page { get; }
43+
internal int DefaultNavigationTimeout { get; set; } = 30000;
3844

3945
#endregion
4046

@@ -51,6 +57,89 @@ internal ExecutionContext ExecutionContextById(int contextId)
5157
return context;
5258
}
5359

60+
public async Task<Response> NavigateFrameAsync(Frame frame, string url, NavigationOptions options)
61+
{
62+
var referrer = string.IsNullOrEmpty(options.Referer)
63+
? _networkManager.ExtraHTTPHeaders?.GetValueOrDefault(MessageKeys.Referer)
64+
: options.Referer;
65+
var requests = new Dictionary<string, Request>();
66+
var timeout = options?.Timeout ?? DefaultNavigationTimeout;
67+
var watcher = new NavigatorWatcher(_client, this, frame, _networkManager, timeout, options);
68+
69+
var navigateTask = NavigateAsync(_client, url, referrer, frame.Id);
70+
await Task.WhenAny(
71+
watcher.TimeoutOrTerminationTask,
72+
navigateTask).ConfigureAwait(false);
73+
74+
AggregateException exception = null;
75+
if (navigateTask.IsFaulted)
76+
{
77+
exception = navigateTask.Exception;
78+
}
79+
else
80+
{
81+
await Task.WhenAny(
82+
watcher.TimeoutOrTerminationTask,
83+
_ensureNewDocumentNavigation ? watcher.NewDocumentNavigationTask : watcher.SameDocumentNavigationTask
84+
).ConfigureAwait(false);
85+
86+
if (watcher.TimeoutOrTerminationTask.IsCompleted && watcher.TimeoutOrTerminationTask.Result.IsFaulted)
87+
{
88+
exception = watcher.TimeoutOrTerminationTask.Result.Exception;
89+
}
90+
}
91+
92+
if (exception != null)
93+
{
94+
throw new NavigationException(exception.InnerException.Message, exception.InnerException);
95+
}
96+
97+
return watcher.NavigationResponse;
98+
}
99+
100+
private async Task NavigateAsync(CDPSession client, string url, string referrer, string frameId)
101+
{
102+
var response = await client.SendAsync<PageNavigateResponse>("Page.navigate", new
103+
{
104+
url,
105+
referrer = referrer ?? string.Empty,
106+
frameId
107+
}).ConfigureAwait(false);
108+
109+
_ensureNewDocumentNavigation = !string.IsNullOrEmpty(response.LoaderId);
110+
111+
if (!string.IsNullOrEmpty(response.ErrorText))
112+
{
113+
throw new NavigationException(response.ErrorText, url);
114+
}
115+
}
116+
117+
public async Task<Response> WaitForFrameNavigationAsync(Frame frame, NavigationOptions options = null)
118+
{
119+
var timeout = options?.Timeout ?? DefaultNavigationTimeout;
120+
var watcher = new NavigatorWatcher(_client, this, frame, _networkManager, timeout, options);
121+
122+
var raceTask = await Task.WhenAny(
123+
watcher.NewDocumentNavigationTask,
124+
watcher.SameDocumentNavigationTask,
125+
watcher.TimeoutOrTerminationTask
126+
).ConfigureAwait(false);
127+
128+
var exception = raceTask.Exception;
129+
if (exception == null &&
130+
watcher.TimeoutOrTerminationTask.IsCompleted &&
131+
watcher.TimeoutOrTerminationTask.Result.IsFaulted)
132+
{
133+
exception = watcher.TimeoutOrTerminationTask.Result.Exception;
134+
}
135+
if (exception != null)
136+
{
137+
throw new NavigationException(exception.Message, exception);
138+
}
139+
140+
return watcher.NavigationResponse;
141+
}
142+
54143
#endregion
55144

56145
#region Private Methods

0 commit comments

Comments
 (0)