Skip to content

Commit f149fa9

Browse files
authored
Multisession network manager (#2437)
* small progress * some progress * Some progress * Some nice progress * Feature complete * code factor * some fixes * some fixes * some other fixes
1 parent f218072 commit f149fa9

22 files changed

+1259
-158
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<a id="navigate-within-document" href="#nav">Navigate within document</a>
2+
<a name="nav"></a>
3+
<script>
4+
fetch('oopif.html?requestFromOOPIF')
5+
</script>

lib/PuppeteerSharp.Tests/NetworkManagerTests/NetworkManagerTests.cs

Lines changed: 825 additions & 0 deletions
Large diffs are not rendered by default.

lib/PuppeteerSharp.Tests/OOPIFTests/OOPIFTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,12 @@ public async Task ShouldLoadOopifIframesWithSubresourcesAndRequestInterception()
250250
var frameTask = Page.WaitForFrameAsync((frame) => frame.Url.EndsWith("/oopif.html"));
251251
await Page.SetRequestInterceptionAsync(true);
252252
Page.Request += (sender, e) => _ = e.Request.ContinueAsync();
253-
253+
var requestTask = Page.WaitForRequestAsync(request => request.Url.Contains("requestFromOOPIF"));
254254
await Page.GoToAsync(TestConstants.ServerUrl + "/dynamic-oopif.html");
255255
await frameTask.WithTimeout();
256+
await requestTask.WithTimeout();
256257
Assert.That(Oopifs, Has.Exactly(1).Items);
258+
Assert.AreEqual(requestTask.Result.Frame, frameTask.Result);
257259
}
258260

259261
[PuppeteerTest("oopif.spec.ts", "OOPIF", "should support frames within OOP iframes")]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using NUnit.Framework;
5+
using PuppeteerSharp.Nunit;
6+
using PuppeteerSharp.Tests.Attributes;
7+
8+
namespace PuppeteerSharp.Tests.PrerenderTests;
9+
10+
public class WithNetworkRequestsTests : PuppeteerPageBaseTest
11+
{
12+
[PuppeteerTest("prerender.spec.ts", "with network requests", "can receive requests from the prerendered page")]
13+
[Skip(SkipAttribute.Targets.Firefox)]
14+
public async Task CanNavigateToAPrerenderedPageViaInput()
15+
{
16+
var urls = new List<string>();
17+
Page.Request += (_, e) => urls.Add(e.Request.Url);
18+
await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/index.html");
19+
20+
var button = await Page.WaitForSelectorAsync("button");
21+
await button.ClickAsync();
22+
23+
var mainFrame = Page.MainFrame;
24+
var link = await mainFrame.WaitForSelectorAsync("a");
25+
await Task.WhenAll(
26+
mainFrame.WaitForNavigationAsync(),
27+
link.ClickAsync()
28+
);
29+
30+
Assert.AreSame(mainFrame, Page.MainFrame);
31+
Assert.AreEqual("target", await mainFrame.EvaluateExpressionAsync<string>("document.body.innerText"));
32+
Assert.AreSame(mainFrame, Page.MainFrame);
33+
34+
Assert.True(urls.Exists(url => url.Contains("prerender/target.html")));
35+
Assert.True(urls.Exists(url => url.Contains("prerender/index.html")));
36+
Assert.True(urls.Exists(url => url.Contains("prerender/target.html?fromPrerendered")));
37+
}
38+
}

lib/PuppeteerSharp.Tests/PuppeteerSharp.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
1111
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
1212
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
13+
<PackageReference Include="NSubstitute" Version="5.1.0" />
1314
<PackageReference Include="NUnit" Version="3.13.3" />
1415
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
1516
<PackageReference Include="System.Net.Http" Version="4.3.4" />

lib/PuppeteerSharp/CDPSession.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using Microsoft.Extensions.Logging;
78
using Newtonsoft.Json.Linq;
@@ -15,6 +16,7 @@ public class CDPSession : ICDPSession
1516
{
1617
private readonly ConcurrentDictionary<int, MessageTask> _callbacks = new();
1718
private readonly string _parentSessionId;
19+
private int _lastId;
1820

1921
internal CDPSession(Connection connection, TargetType targetType, string sessionId, string parentSessionId)
2022
{
@@ -81,7 +83,7 @@ public async Task<JObject> SendAsync(string method, object args = null, bool wai
8183
CloseReason);
8284
}
8385

84-
var id = Connection.GetMessageID();
86+
var id = GetMessageID();
8587
var message = Connection.GetMessage(id, method, args, Id);
8688

8789
MessageTask callback = null;
@@ -148,6 +150,8 @@ internal void OnMessage(ConnectionResponse obj)
148150
}
149151
}
150152

153+
internal int GetMessageID() => Interlocked.Increment(ref _lastId);
154+
151155
internal void Close(string closeReason)
152156
{
153157
if (IsClosed)

lib/PuppeteerSharp/ChromeTargetManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ private async Task OnAttachedToTargetAsync(object sender, TargetAttachedToTarget
288288
_attachedTargetsBySessionId.TryAdd(session.Id, target);
289289
}
290290

291-
(parentConnection as CDPSession)?.OnSessionReady(session);
291+
(parentSession ?? parentConnection as CDPSession)?.OnSessionReady(session);
292292

293293
await EnsureTargetsIdsForInitAsync().ConfigureAwait(false);
294294
_targetsIdsForInit.Remove(target.TargetId);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// * MIT License
2+
// *
3+
// * Copyright (c) Microsoft Corporation.
4+
// *
5+
// * Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// * of this software and associated documentation files (the "Software"), to deal
7+
// * in the Software without restriction, including without limitation the rights
8+
// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// * copies of the Software, and to permit persons to whom the Software is
10+
// * furnished to do so, subject to the following conditions:
11+
// *
12+
// * The above copyright notice and this permission notice shall be included in all
13+
// * copies or substantial portions of the Software.
14+
// *
15+
// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// * SOFTWARE.
22+
23+
using System;
24+
using System.Collections.Generic;
25+
26+
namespace PuppeteerSharp;
27+
28+
/// <summary>
29+
/// This is a DisposableStack in puppeteer. We don't need to dispose objects manually.
30+
/// But this is used to run actions when this object is disposed.
31+
/// </summary>
32+
internal class DisposableActionsStack : IDisposable
33+
{
34+
private readonly List<Action> _actions = [];
35+
36+
public bool IsDisposed { get; private set; }
37+
38+
public void Dispose()
39+
{
40+
if (IsDisposed)
41+
{
42+
return;
43+
}
44+
45+
IsDisposed = true;
46+
_actions.Reverse();
47+
48+
foreach (var action in _actions)
49+
{
50+
action();
51+
}
52+
}
53+
54+
public void Defer(Action action)
55+
{
56+
_actions.Add(action);
57+
}
58+
}

lib/PuppeteerSharp/FrameManager.cs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace PuppeteerSharp
1313
{
14-
internal class FrameManager : IDisposable, IAsyncDisposable
14+
internal class FrameManager : IDisposable, IAsyncDisposable, IFrameProvider
1515
{
1616
private const int TimeForWaitingForSwap = 200;
1717
private const string UtilityWorldName = "__puppeteer_utility_world__";
@@ -28,7 +28,7 @@ internal FrameManager(CDPSession client, Page page, bool ignoreHTTPSErrors, Time
2828
Client = client;
2929
Page = page;
3030
_logger = Client.Connection.LoggerFactory.CreateLogger<FrameManager>();
31-
NetworkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
31+
NetworkManager = new NetworkManager(ignoreHTTPSErrors, this, client.Connection.LoggerFactory);
3232
TimeoutSettings = timeoutSettings;
3333

3434
Client.MessageReceived += Client_MessageReceived;
@@ -59,10 +59,7 @@ internal FrameManager(CDPSession client, Page page, bool ignoreHTTPSErrors, Time
5959

6060
internal Frame MainFrame => FrameTree.MainFrame;
6161

62-
public void Dispose()
63-
{
64-
_eventsQueue?.Dispose();
65-
}
62+
public void Dispose() => _eventsQueue?.Dispose();
6663

6764
public async ValueTask DisposeAsync()
6865
{
@@ -72,6 +69,8 @@ public async ValueTask DisposeAsync()
7269
}
7370
}
7471

72+
public Task<Frame> GetFrameAsync(string frameId) => FrameTree.TryGetFrameAsync(frameId);
73+
7574
internal ExecutionContext ExecutionContextById(int contextId, CDPSession session = null)
7675
{
7776
session ??= Client;
@@ -111,11 +110,11 @@ internal DeviceRequestPromptManager GetDeviceRequestPromptManager(CDPSession cli
111110

112111
internal Frame[] GetFrames() => FrameTree.Frames;
113112

114-
internal async Task InitializeAsync(CDPSession client = null)
113+
internal async Task InitializeAsync(CDPSession client)
115114
{
116115
try
117116
{
118-
client ??= Client;
117+
var networkInitTask = NetworkManager.AddClientAsync(client);
119118
var getFrameTreeTask = client.SendAsync<PageGetFrameTreeResponse>("Page.getFrameTree");
120119
var autoAttachTask = client != Client
121120
? client.SendAsync("Target.setAutoAttach", new TargetSetAutoAttachRequest
@@ -136,7 +135,7 @@ await Task.WhenAll(
136135
await Task.WhenAll(
137136
client.SendAsync("Page.setLifecycleEventsEnabled", new PageSetLifecycleEventsEnabledRequest { Enabled = true }),
138137
client.SendAsync("Runtime.enable"),
139-
client == Client ? NetworkManager.InitializeAsync() : Task.CompletedTask).ConfigureAwait(false);
138+
networkInitTask).ConfigureAwait(false);
140139

141140
await CreateIsolatedWorldAsync(client, UtilityWorldName).ConfigureAwait(false);
142141
}
@@ -154,11 +153,12 @@ await Task.WhenAll(
154153
}
155154
}
156155

157-
/**
158-
* When the main frame is replaced by another main frame,
159-
* we maintain the main frame object identity while updating
160-
* its frame tree and ID.
161-
*/
156+
/// <summary>
157+
/// When the main frame is replaced by another main frame
158+
/// we maintain the main frame object identity while updating
159+
/// its frame tree and ID.
160+
/// </summary>
161+
/// <param name="client">New session.</param>
162162
internal async Task SwapFrameTreeAsync(CDPSession client)
163163
{
164164
OnExecutionContextsCleared(Client);
@@ -181,11 +181,14 @@ internal async Task SwapFrameTreeAsync(CDPSession client)
181181
Client.Disconnected += (sender, e) => _ = OnClientDisconnectAsync();
182182

183183
await InitializeAsync(client).ConfigureAwait(false);
184-
await NetworkManager.UpdateClientAsync(client).ConfigureAwait(false);
184+
await NetworkManager.AddClientAsync(client).ConfigureAwait(false);
185185

186186
frame?.OnFrameSwappedByActivation();
187187
}
188188

189+
internal Task RegisterSpeculativeSessionAsync(CDPSession client)
190+
=> NetworkManager.AddClientAsync(client);
191+
189192
private Frame GetFrame(string frameId) => FrameTree.GetById(frameId);
190193

191194
private void Client_MessageReceived(object sender, MessageEventArgs e)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// * MIT License
2+
// *
3+
// * Copyright (c) Darío Kondratiuk
4+
// *
5+
// * Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// * of this software and associated documentation files (the "Software"), to deal
7+
// * in the Software without restriction, including without limitation the rights
8+
// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// * copies of the Software, and to permit persons to whom the Software is
10+
// * furnished to do so, subject to the following conditions:
11+
// *
12+
// * The above copyright notice and this permission notice shall be included in all
13+
// * copies or substantial portions of the Software.
14+
// *
15+
// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// * SOFTWARE.
22+
23+
using System.Threading.Tasks;
24+
25+
namespace PuppeteerSharp;
26+
27+
/// <summary>
28+
/// Provides a way to get a frame.
29+
/// </summary>
30+
internal interface IFrameProvider
31+
{
32+
/// <summary>
33+
/// Gets a frame by its ID.
34+
/// </summary>
35+
/// <param name="frameId">The frame ID.</param>
36+
/// <returns>A <see cref="Task"/> that completes when the frame is retrieved.</returns>
37+
Task<Frame> GetFrameAsync(string frameId);
38+
}

0 commit comments

Comments
 (0)