Skip to content

Commit d215560

Browse files
authored
Support tab targets (#2419)
* some progress * some progress * Support tab targets * add missing setup listeners * Fix a test
1 parent 61b68a2 commit d215560

30 files changed

+404
-108
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<script>
4+
function addRules() {
5+
const script = document.createElement('script');
6+
script.type = 'speculationrules';
7+
script.innerText = `
8+
{
9+
"prerender": [
10+
{"source": "list", "urls": ["target.html"]}
11+
]
12+
}
13+
`;
14+
document.head.append(script);
15+
}
16+
</script>
17+
</head>
18+
<body>
19+
<button onclick="addRules()">add rules</button>
20+
<a href="target.html">test</a>
21+
</body>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<script>fetch('target.html?fromPrerendered')</script>
4+
</head>
5+
<body>target<input></input></body>

lib/PuppeteerSharp.Tests/CoverageTests/JSCoverageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public async Task ShouldIgnoreEvalScriptsByDefault()
6565
Assert.That(coverage, Has.Exactly(1).Items);
6666
}
6767

68-
[PuppeteerTest("coverage.spec.ts", "JSCoverage", "shouldn't ignore eval() scripts if reportAnonymousScripts is true")]
68+
[PuppeteerTest("coverage.spec.ts", "JSCoverage", "should not ignore eval() scripts if reportAnonymousScripts is true")]
6969
[Skip(SkipAttribute.Targets.Firefox)]
7070
public async Task ShouldNotIgnoreEvalScriptsIfReportAnonymousScriptsIsTrue()
7171
{

lib/PuppeteerSharp.Tests/LauncherTests/BrowserDisconnectTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public async Task ShouldRejectNavigationWhenBrowserCloses()
3232
{
3333
"Navigating frame was detached",
3434
"Protocol error(Page.navigate): Target closed. (Connection disposed)",
35-
}.Any(value => exception.Message.Contains(value)));
35+
}.Any(value => exception!.Message.Contains(value)));
3636
}
3737

3838
[PuppeteerTest("launcher.spec.ts", "Browser.disconnect", "should reject waitForSelector when browser closes")]
@@ -50,7 +50,7 @@ public async Task ShouldRejectWaitForSelectorWhenBrowserCloses()
5050
var watchdog = page.WaitForSelectorAsync("div", new WaitForSelectorOptions { Timeout = 60000 });
5151
remote.Disconnect();
5252
var exception = Assert.ThrowsAsync<WaitTaskTimeoutException>(() => watchdog);
53-
Assert.True(exception.Message.Contains("frame got detached"));
53+
Assert.True(exception!.Message.Contains("Session closed"));
5454
}
5555
}
5656
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Threading.Tasks;
2+
using NUnit.Framework;
3+
using PuppeteerSharp.Nunit;
4+
using PuppeteerSharp.Tests.Attributes;
5+
6+
namespace PuppeteerSharp.Tests.PrerenderTests;
7+
8+
public class PrerenderTests : PuppeteerPageBaseTest
9+
{
10+
[PuppeteerTest("prerender.spec.ts", "Prerender", "can navigate to a prerendered page via input")]
11+
[Skip(SkipAttribute.Targets.Firefox)]
12+
public async Task CanNavigateToAPrerenderedPageViaInput()
13+
{
14+
await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/index.html");
15+
16+
var button = await Page.WaitForSelectorAsync("button");
17+
await button.ClickAsync();
18+
19+
var link = await Page.WaitForSelectorAsync("a");
20+
await Task.WhenAll(
21+
Page.WaitForNavigationAsync(),
22+
link.ClickAsync()
23+
);
24+
25+
Assert.AreEqual("target", await Page.EvaluateExpressionAsync<string>("document.body.innerText"));
26+
}
27+
28+
[PuppeteerTest("prerender.spec.ts", "Prerender", "can navigate to a prerendered page via Puppeteer")]
29+
[Skip(SkipAttribute.Targets.Firefox)]
30+
public async Task CanNavigateToAPrerenderedPageViaPuppeteer()
31+
{
32+
await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/index.html");
33+
34+
var button = await Page.WaitForSelectorAsync("button");
35+
await button.ClickAsync();
36+
37+
38+
await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/target.html");
39+
Assert.AreEqual("target", await Page.EvaluateExpressionAsync<string>("document.body.innerText"));
40+
}
41+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Threading.Tasks;
2+
using NUnit.Framework;
3+
using PuppeteerSharp.Nunit;
4+
using PuppeteerSharp.Tests.Attributes;
5+
6+
namespace PuppeteerSharp.Tests.PrerenderTests;
7+
8+
public class ViaFrameTests : PuppeteerPageBaseTest
9+
{
10+
[PuppeteerTest("prerender.spec.ts", "via frame", "can navigate to a prerendered page via input")]
11+
[Skip(SkipAttribute.Targets.Firefox)]
12+
public async Task CanNavigateToAPrerenderedPageViaInput()
13+
{
14+
await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/index.html");
15+
16+
var button = await Page.WaitForSelectorAsync("button");
17+
await button.ClickAsync();
18+
19+
var mainFrame = Page.MainFrame;
20+
var link = await mainFrame.WaitForSelectorAsync("a");
21+
await Task.WhenAll(
22+
mainFrame.WaitForNavigationAsync(),
23+
link.ClickAsync()
24+
);
25+
26+
Assert.AreSame(mainFrame, Page.MainFrame);
27+
Assert.AreEqual("target", await mainFrame.EvaluateExpressionAsync<string>("document.body.innerText"));
28+
Assert.AreSame(mainFrame, Page.MainFrame);
29+
}
30+
31+
[PuppeteerTest("prerender.spec.ts", "via frame", "can navigate to a prerendered page via Puppeteer")]
32+
[Skip(SkipAttribute.Targets.Firefox)]
33+
public async Task CanNavigateToAPrerenderedPageViaPuppeteer()
34+
{
35+
await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/index.html");
36+
37+
var button = await Page.WaitForSelectorAsync("button");
38+
await button.ClickAsync();
39+
40+
var mainFrame = Page.MainFrame;
41+
await mainFrame.GoToAsync(TestConstants.ServerUrl + "/prerender/target.html");
42+
Assert.AreSame(mainFrame, Page.MainFrame);
43+
Assert.AreEqual("target", await mainFrame.EvaluateExpressionAsync<string>("document.body.innerText"));
44+
Assert.AreSame(mainFrame, Page.MainFrame);
45+
}
46+
}

lib/PuppeteerSharp.Tests/PuppeteerPageBaseTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ protected Task WaitForError()
2121
{
2222
var wrapper = new TaskCompletionSource<bool>();
2323

24-
void errorEvent(object sender, ErrorEventArgs e)
24+
void ErrorEvent(object sender, ErrorEventArgs e)
2525
{
2626
wrapper.SetResult(true);
27-
Page.Error -= errorEvent;
27+
Page.Error -= ErrorEvent;
2828
}
2929

30-
Page.Error += errorEvent;
30+
Page.Error += ErrorEvent;
3131

3232
return wrapper.Task;
3333
}

lib/PuppeteerSharp/Browser.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,8 @@ internal async Task<IPage> CreatePageInContextAsync(string contextId)
332332

333333
var targetId = (await Connection.SendAsync<TargetCreateTargetResponse>("Target.createTarget", createTargetRequest)
334334
.ConfigureAwait(false)).TargetId;
335-
var target = await TargetManager.GetAvailableTargets().GetItemAsync(targetId).ConfigureAwait(false);
336-
await target.InitializedTask.ConfigureAwait(false);
335+
var target = await WaitForTargetAsync(t => t.TargetId == targetId).ConfigureAwait(false) as Target;
336+
await target!.InitializedTask.ConfigureAwait(false);
337337
return await target.PageAsync().ConfigureAwait(false);
338338
}
339339

@@ -482,7 +482,7 @@ private void Detach()
482482
TargetManager.TargetDiscovered -= TargetManager_TargetDiscovered;
483483
}
484484

485-
private Target CreateTarget(TargetInfo targetInfo, CDPSession session)
485+
private Target CreateTarget(TargetInfo targetInfo, CDPSession session, CDPSession parentSession)
486486
{
487487
var browserContextId = targetInfo.BrowserContextId;
488488

@@ -491,14 +491,14 @@ private Target CreateTarget(TargetInfo targetInfo, CDPSession session)
491491
context = _defaultContext;
492492
}
493493

494-
Func<bool, Task<CDPSession>> createSession = (bool isAutoAttachEmulated) => Connection.CreateSessionAsync(targetInfo, isAutoAttachEmulated);
494+
Task<CDPSession> CreateSession(bool isAutoAttachEmulated) => Connection.CreateSessionAsync(targetInfo, isAutoAttachEmulated);
495495

496496
var otherTarget = new OtherTarget(
497497
targetInfo,
498498
session,
499499
context,
500500
TargetManager,
501-
createSession);
501+
CreateSession);
502502

503503
if (targetInfo.Url?.StartsWith("devtools://", StringComparison.OrdinalIgnoreCase) == true)
504504
{
@@ -507,7 +507,7 @@ private Target CreateTarget(TargetInfo targetInfo, CDPSession session)
507507
session,
508508
context,
509509
TargetManager,
510-
createSession,
510+
CreateSession,
511511
IgnoreHTTPSErrors,
512512
DefaultViewport,
513513
ScreenshotTaskQueue);
@@ -520,7 +520,7 @@ private Target CreateTarget(TargetInfo targetInfo, CDPSession session)
520520
session,
521521
context,
522522
TargetManager,
523-
createSession,
523+
CreateSession,
524524
IgnoreHTTPSErrors,
525525
DefaultViewport,
526526
ScreenshotTaskQueue);
@@ -533,7 +533,7 @@ private Target CreateTarget(TargetInfo targetInfo, CDPSession session)
533533
session,
534534
context,
535535
TargetManager,
536-
createSession);
536+
CreateSession);
537537
}
538538

539539
return otherTarget;

lib/PuppeteerSharp/CDPSession.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ namespace PuppeteerSharp
1414
public class CDPSession : ICDPSession
1515
{
1616
private readonly ConcurrentDictionary<int, MessageTask> _callbacks = new();
17+
private readonly string _parentSessionId;
1718

18-
internal CDPSession(Connection connection, TargetType targetType, string sessionId)
19+
internal CDPSession(Connection connection, TargetType targetType, string sessionId, string parentSessionId)
1920
{
2021
Connection = connection;
2122
TargetType = targetType;
2223
Id = sessionId;
24+
_parentSessionId = parentSessionId;
2325
}
2426

2527
/// <inheritdoc/>
@@ -36,6 +38,8 @@ internal CDPSession(Connection connection, TargetType targetType, string session
3638

3739
internal event EventHandler<SessionEventArgs> Ready;
3840

41+
internal event EventHandler<SessionEventArgs> Swapped;
42+
3943
/// <inheritdoc/>
4044
public TargetType TargetType { get; }
4145

@@ -55,6 +59,9 @@ internal CDPSession(Connection connection, TargetType targetType, string session
5559

5660
internal Target Target { get; set; }
5761

62+
internal CDPSession ParentSession
63+
=> string.IsNullOrEmpty(_parentSessionId) ? this : Connection.GetSession(_parentSessionId) ?? this;
64+
5865
/// <inheritdoc/>
5966
public async Task<T> SendAsync<T>(string method, object args = null)
6067
{
@@ -170,5 +177,7 @@ internal void OnSessionDetached(CDPSession session)
170177
=> SessionDetached?.Invoke(this, new SessionEventArgs(session));
171178

172179
internal IEnumerable<MessageTask> GetPendingMessages() => _callbacks.Values;
180+
181+
internal void OnSwapped(CDPSession session) => Swapped?.Invoke(this, new SessionEventArgs(session));
173182
}
174183
}

lib/PuppeteerSharp/ChromeTargetManager.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class ChromeTargetManager : ITargetManager
1313
{
1414
private readonly List<string> _ignoredTargets = new();
1515
private readonly Connection _connection;
16-
private readonly Func<TargetInfo, CDPSession, Target> _targetFactoryFunc;
16+
private readonly Func<TargetInfo, CDPSession, CDPSession, Target> _targetFactoryFunc;
1717
private readonly Func<Target, bool> _targetFilterFunc;
1818
private readonly ILogger<ChromeTargetManager> _logger;
1919
private readonly AsyncDictionaryHelper<string, Target> _attachedTargetsByTargetId = new("Target {0} not found");
@@ -28,7 +28,7 @@ internal class ChromeTargetManager : ITargetManager
2828

2929
public ChromeTargetManager(
3030
Connection connection,
31-
Func<TargetInfo, CDPSession, Target> targetFactoryFunc,
31+
Func<TargetInfo, CDPSession, CDPSession, Target> targetFactoryFunc,
3232
Func<Target, bool> targetFilterFunc,
3333
int targetDiscoveryTimeout = 0)
3434
{
@@ -168,7 +168,7 @@ private void OnTargetCreated(TargetCreatedResponse e)
168168
return;
169169
}
170170

171-
var target = _targetFactoryFunc(e.TargetInfo, null);
171+
var target = _targetFactoryFunc(e.TargetInfo, null, null);
172172
target.Initialize();
173173
_attachedTargetsByTargetId.AddItem(e.TargetInfo.TargetId, target);
174174
}
@@ -206,6 +206,13 @@ private void OnTargetInfoChanged(TargetCreatedResponse e)
206206

207207
var previousURL = target.Url;
208208
var wasInitialized = target.IsInitialized;
209+
210+
if (IsPageTargetBecomingPrimary(target, e.TargetInfo))
211+
{
212+
var session = target.Session;
213+
session.ParentSession?.OnSwapped(session);
214+
}
215+
209216
target.TargetInfoChanged(e.TargetInfo);
210217

211218
if (wasInitialized && previousURL != target.Url)
@@ -218,9 +225,13 @@ private void OnTargetInfoChanged(TargetCreatedResponse e)
218225
}
219226
}
220227

228+
private bool IsPageTargetBecomingPrimary(Target target, TargetInfo newTargetInfo)
229+
=> !string.IsNullOrEmpty(target.TargetInfo.Subtype) && string.IsNullOrEmpty(newTargetInfo.Subtype);
230+
221231
private async Task OnAttachedToTargetAsync(object sender, TargetAttachedToTargetResponse e)
222232
{
223-
var parentSession = sender as ICDPConnection;
233+
var parentConnection = sender as ICDPConnection;
234+
var parentSession = sender as CDPSession;
224235

225236
var targetInfo = e.TargetInfo;
226237
var session = _connection.GetSession(e.SessionId) ?? throw new PuppeteerException($"Session {e.SessionId} was not created.");
@@ -240,7 +251,7 @@ private async Task OnAttachedToTargetAsync(object sender, TargetAttachedToTarget
240251
return;
241252
}
242253

243-
var workerTarget = _targetFactoryFunc(targetInfo, null);
254+
var workerTarget = _targetFactoryFunc(targetInfo, null, null);
244255
workerTarget.Initialize();
245256
_attachedTargetsByTargetId.AddItem(targetInfo.TargetId, workerTarget);
246257
TargetAvailable?.Invoke(this, new TargetChangedArgs { Target = workerTarget });
@@ -250,7 +261,7 @@ private async Task OnAttachedToTargetAsync(object sender, TargetAttachedToTarget
250261
var isExistingTarget = _attachedTargetsByTargetId.TryGetValue(targetInfo.TargetId, out var target);
251262
if (!isExistingTarget)
252263
{
253-
target = _targetFactoryFunc(targetInfo, session);
264+
target = _targetFactoryFunc(targetInfo, session, parentSession);
254265
}
255266

256267
if (_targetFilterFunc?.Invoke(target) == false)
@@ -277,7 +288,7 @@ private async Task OnAttachedToTargetAsync(object sender, TargetAttachedToTarget
277288
_attachedTargetsBySessionId.TryAdd(session.Id, target);
278289
}
279290

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

282293
await EnsureTargetsIdsForInitAsync().ConfigureAwait(false);
283294
_targetsIdsForInit.Remove(target.TargetId);
@@ -312,7 +323,7 @@ async Task SilentDetach()
312323
try
313324
{
314325
await session.SendAsync("Runtime.runIfWaitingForDebugger").ConfigureAwait(false);
315-
await parentSession!.SendAsync(
326+
await parentConnection!.SendAsync(
316327
"Target.detachFromTarget",
317328
new TargetDetachFromTargetRequest
318329
{

0 commit comments

Comments
 (0)