Skip to content

Commit f73ccd8

Browse files
kblokclaude
andcommitted
Implement upstream PR #14824: adds extension realms API
Ports puppeteer/puppeteer#14824 to PuppeteerSharp. Adds ExtensionRealms() to Page/Frame, Origin and ExtensionAsync() to Realm, and extension world tracking in CdpFrame/FrameManager. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d6ee78b commit f73ccd8

21 files changed

Lines changed: 482 additions & 19 deletions
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.IO;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using NUnit.Framework;
5+
using PuppeteerSharp.Nunit;
6+
7+
namespace PuppeteerSharp.Tests.RealmsTests
8+
{
9+
public class RealmsTests : PuppeteerBaseTest
10+
{
11+
private static readonly string _extensionPath = Path.Combine(AppContext.BaseDirectory, "Assets", "simple-extension");
12+
13+
private static LaunchOptions BrowserWithExtensionOptions() => new()
14+
{
15+
Headless = false,
16+
EnableExtensions = true,
17+
Pipe = true,
18+
};
19+
20+
[Test, PuppeteerTest("cdp/realms.spec", "extension realms", "should include content script realms")]
21+
public async Task ShouldIncludeContentScriptRealms()
22+
{
23+
await using var browserWithExtension = await Puppeteer.LaunchAsync(
24+
BrowserWithExtensionOptions(),
25+
TestConstants.LoggerFactory);
26+
27+
var page = await browserWithExtension.NewPageAsync();
28+
var extId = await browserWithExtension.InstallExtensionAsync(_extensionPath);
29+
await browserWithExtension.WaitForTargetAsync(t => t.Url.Contains(extId));
30+
31+
await page.GoToAsync(TestConstants.EmptyPage);
32+
33+
var realms = page.ExtensionRealms();
34+
Assert.That(realms.Count, Is.GreaterThanOrEqualTo(1));
35+
}
36+
37+
[Test, PuppeteerTest("cdp/realms.spec", "extension realms", "realm should return extension that created it")]
38+
public async Task RealmShouldReturnExtensionThatCreatedIt()
39+
{
40+
await using var browserWithExtension = await Puppeteer.LaunchAsync(
41+
BrowserWithExtensionOptions(),
42+
TestConstants.LoggerFactory);
43+
44+
var page = await browserWithExtension.NewPageAsync();
45+
var extId = await browserWithExtension.InstallExtensionAsync(_extensionPath);
46+
await browserWithExtension.WaitForTargetAsync(t => t.Url.Contains(extId));
47+
48+
await page.GoToAsync(TestConstants.EmptyPage);
49+
var realms = page.ExtensionRealms();
50+
51+
Realm realm = null;
52+
foreach (var r in realms)
53+
{
54+
var ext = await r.ExtensionAsync();
55+
if (ext != null && ext.Id == extId)
56+
{
57+
realm = r;
58+
break;
59+
}
60+
}
61+
62+
Assert.That(realm, Is.Not.Null);
63+
var extension = await realm.ExtensionAsync();
64+
Assert.That(extension, Is.Not.Null);
65+
Assert.That(extension.Id, Is.EqualTo(extId));
66+
}
67+
68+
[Test, PuppeteerTest("cdp/realms.spec", "extension realms", "should evaluate in content script realms")]
69+
public async Task ShouldEvaluateInContentScriptRealms()
70+
{
71+
await using var browserWithExtension = await Puppeteer.LaunchAsync(
72+
BrowserWithExtensionOptions(),
73+
TestConstants.LoggerFactory);
74+
75+
var page = await browserWithExtension.NewPageAsync();
76+
var extId = await browserWithExtension.InstallExtensionAsync(_extensionPath);
77+
await browserWithExtension.WaitForTargetAsync(t => t.Url.Contains(extId));
78+
79+
await page.GoToAsync(TestConstants.EmptyPage);
80+
var realms = page.ExtensionRealms();
81+
82+
Realm contentScriptRealm = null;
83+
foreach (var r in realms)
84+
{
85+
var ext = await r.ExtensionAsync();
86+
if (ext != null && ext.Id == extId)
87+
{
88+
contentScriptRealm = r;
89+
break;
90+
}
91+
}
92+
93+
Assert.That(contentScriptRealm, Is.Not.Null);
94+
95+
var isContentScript = await contentScriptRealm.EvaluateFunctionAsync<bool>("() => globalThis.thisIsTheContentScript");
96+
Assert.That(isContentScript, Is.True);
97+
98+
var isContentScriptInMain = await page.EvaluateFunctionAsync<object>("() => globalThis.thisIsTheContentScript");
99+
Assert.That(isContentScriptInMain, Is.Null);
100+
}
101+
}
102+
}

lib/PuppeteerSharp/Bidi/BidiBrowser.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
using System;
2626
using System.Collections.Concurrent;
27+
using System.Collections.Generic;
2728
using System.Diagnostics.CodeAnalysis;
2829
using System.Linq;
2930
using System.Threading.Tasks;
@@ -201,6 +202,10 @@ await Driver.WebExtension.UninstallAsync(
201202
new WebDriverBiDi.WebExtension.UninstallCommandParameters(id)).ConfigureAwait(false);
202203
}
203204

205+
/// <inheritdoc/>
206+
public override Task<IReadOnlyDictionary<string, Extension>> GetExtensionsAsync()
207+
=> throw new NotSupportedException("GetExtensions is not supported in WebDriver BiDi.");
208+
204209
/// <inheritdoc />
205210
public override ITarget[] Targets()
206211
=>

lib/PuppeteerSharp/Bidi/BidiFrame.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,10 @@ internal async Task<IList<NodeRemoteValue>> LocateNodesAsync(BidiElementHandle e
610610
[element.Value.ConvertTo<NodeRemoteValue>().ToSharedReference()]).ConfigureAwait(false);
611611
}
612612

613+
/// <inheritdoc />
614+
public override IReadOnlyList<Realm> ExtensionRealms()
615+
=> throw new NotSupportedException("ExtensionRealms is not supported in WebDriver BiDi.");
616+
613617
/// <inheritdoc />
614618
protected internal override DeviceRequestPromptManager GetDeviceRequestPromptManager() => throw new System.NotImplementedException();
615619

lib/PuppeteerSharp/Bidi/BidiPage.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,10 @@ public override async Task RemoveScriptToEvaluateOnNewDocumentAsync(string ident
880880
/// <inheritdoc />
881881
public override Task SetBypassServiceWorkerAsync(bool bypass) => throw new NotImplementedException();
882882

883+
/// <inheritdoc />
884+
public override IReadOnlyList<Realm> ExtensionRealms()
885+
=> throw new NotSupportedException("ExtensionRealms is not supported in WebDriver BiDi.");
886+
883887
/// <inheritdoc />
884888
public override async Task<NewDocumentScriptEvaluation> EvaluateExpressionOnNewDocumentAsync(string expression)
885889
{

lib/PuppeteerSharp/Bidi/BidiRealm.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ internal class BidiRealm(Core.Realm realm, TimeoutSettings timeoutSettings) : Re
4545

4646
public JSHandle InternalPuppeteerUtilHandle { get; set; }
4747

48+
/// <inheritdoc/>
49+
public override string Origin => throw new NotSupportedException("Origin is not supported in WebDriver BiDi.");
50+
4851
internal override IEnvironment Environment { get; }
4952

53+
/// <inheritdoc/>
54+
public override Task<Extension> ExtensionAsync()
55+
=> throw new NotSupportedException("Extension is not supported in WebDriver BiDi.");
56+
5057
public void Dispose()
5158
{
5259
Disposed = true;

lib/PuppeteerSharp/Browser.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ public abstract class Browser : IBrowser
105105
/// <inheritdoc/>
106106
public abstract Task UninstallExtensionAsync(string id);
107107

108+
/// <inheritdoc/>
109+
public abstract Task<IReadOnlyDictionary<string, Extension>> GetExtensionsAsync();
110+
108111
/// <inheritdoc/>
109112
public async Task<IPage[]> PagesAsync(bool includeAll = false)
110113
=> (await Task.WhenAll(

lib/PuppeteerSharp/Cdp/CdpBrowser.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class CdpBrowser : Browser
3737
private readonly ILogger<Browser> _logger;
3838
private readonly bool _handleDevToolsAsPage;
3939
private readonly bool _networkEnabled;
40+
private readonly Dictionary<string, Extension> _extensions = new();
4041
private Task _closeTask;
4142

4243
internal CdpBrowser(
@@ -207,13 +208,44 @@ public override async Task<string> InstallExtensionAsync(string path)
207208
var response = await Connection.SendAsync<ExtensionsLoadUnpackedResponse>(
208209
"Extensions.loadUnpacked",
209210
new ExtensionsLoadUnpackedRequest { Path = path }).ConfigureAwait(false);
211+
_extensions.Remove(response.Id);
210212
return response.Id;
211213
}
212214

213215
/// <inheritdoc/>
214216
public override async Task UninstallExtensionAsync(string id)
215217
{
216218
await Connection.SendAsync("Extensions.uninstall", new ExtensionsUninstallRequest { Id = id }).ConfigureAwait(false);
219+
_extensions.Remove(id);
220+
}
221+
222+
/// <inheritdoc/>
223+
public override async Task<IReadOnlyDictionary<string, Extension>> GetExtensionsAsync()
224+
{
225+
var response = await Connection.SendAsync<ExtensionsGetExtensionsResponse>("Extensions.getExtensions")
226+
.ConfigureAwait(false);
227+
228+
var extensionsMap = new Dictionary<string, Extension>();
229+
230+
foreach (var info in response.Extensions)
231+
{
232+
if (_extensions.TryGetValue(info.Id, out var existing))
233+
{
234+
extensionsMap[info.Id] = existing;
235+
}
236+
else
237+
{
238+
extensionsMap[info.Id] = new CdpExtension(info.Id, info.Version, info.Name, this);
239+
}
240+
}
241+
242+
_extensions.Clear();
243+
foreach (var kvp in extensionsMap)
244+
{
245+
_extensions[kvp.Key] = kvp.Value;
246+
}
247+
248+
return extensionsMap;
217249
}
218250

219251
internal static async Task<CdpBrowser> CreateAsync(
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using PuppeteerSharp.Cdp.Messaging;
5+
6+
namespace PuppeteerSharp.Cdp;
7+
8+
/// <summary>
9+
/// CDP implementation of <see cref="Extension"/>.
10+
/// </summary>
11+
internal class CdpExtension : Extension
12+
{
13+
private readonly CdpBrowser _browser;
14+
15+
internal CdpExtension(string id, string version, string name, CdpBrowser browser)
16+
: base(id, version, name)
17+
{
18+
_browser = browser;
19+
}
20+
21+
/// <inheritdoc/>
22+
public override async Task<IReadOnlyList<WebWorker>> WorkersAsync()
23+
{
24+
var targets = _browser.Targets();
25+
26+
var workers = new List<WebWorker>();
27+
foreach (var target in targets)
28+
{
29+
var targetUrl = target.Url;
30+
if (target.Type == TargetType.ServiceWorker &&
31+
targetUrl.StartsWith("chrome-extension://" + Id, System.StringComparison.Ordinal))
32+
{
33+
var worker = await target.WorkerAsync().ConfigureAwait(false);
34+
if (worker != null)
35+
{
36+
workers.Add(worker);
37+
}
38+
}
39+
}
40+
41+
return workers;
42+
}
43+
44+
/// <inheritdoc/>
45+
public override async Task<IReadOnlyList<IPage>> PagesAsync()
46+
{
47+
var targets = _browser.Targets();
48+
49+
var pages = new List<IPage>();
50+
foreach (var target in targets)
51+
{
52+
var targetUrl = target.Url;
53+
if ((target.Type == TargetType.Page || target.Type == TargetType.BackgroundPage) &&
54+
targetUrl.StartsWith("chrome-extension://" + Id, System.StringComparison.Ordinal))
55+
{
56+
var page = await target.PageAsync().ConfigureAwait(false);
57+
if (page != null)
58+
{
59+
pages.Add(page);
60+
}
61+
}
62+
}
63+
64+
return pages;
65+
}
66+
67+
/// <inheritdoc/>
68+
public override async Task TriggerActionAsync(IPage page)
69+
{
70+
var cdpPage = (CdpPage)page;
71+
await _browser.Connection.SendAsync("Extensions.triggerAction", new ExtensionsTriggerActionRequest
72+
{
73+
Id = Id,
74+
TargetId = cdpPage.TabId,
75+
}).ConfigureAwait(false);
76+
}
77+
}

lib/PuppeteerSharp/Cdp/CdpFrame.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
// * SOFTWARE.
2222

2323
using System;
24+
using System.Collections.Concurrent;
2425
using System.Collections.Generic;
2526
using System.Linq;
2627
using System.Text.RegularExpressions;
@@ -73,6 +74,8 @@ internal CdpFrame(FrameManager frameManager, string frameId, string parentFrameI
7374

7475
internal CdpPage CdpPage => Page as CdpPage;
7576

77+
internal ConcurrentDictionary<string, IsolatedWorld> ExtensionWorlds { get; } = new();
78+
7679
internal override Frame ParentFrame => FrameManager.FrameTree.GetParentFrame(Id);
7780

7881
internal CdpCDPSession CdpCDPSession => (CdpCDPSession)Client;
@@ -272,6 +275,10 @@ public override async Task<ElementHandle> FrameElementAsync()
272275
return (ElementHandle)await parentFrame.MainRealm.AdoptBackendNodeAsync(response.BackendNodeId).ConfigureAwait(false);
273276
}
274277

278+
/// <inheritdoc/>
279+
public override IReadOnlyList<Realm> ExtensionRealms()
280+
=> ExtensionWorlds.Values.Cast<Realm>().ToList();
281+
275282
internal bool IsOopFrame() => Client != FrameManager.Client;
276283

277284
internal async Task AddPreloadScriptAsync(CdpPreloadScript preloadScript)
@@ -372,6 +379,17 @@ internal void UpdateClient(CDPSession client, bool keepWorlds = false)
372379
}
373380
}
374381

382+
/// <inheritdoc/>
383+
protected internal override void OnDetach()
384+
{
385+
foreach (var world in ExtensionWorlds.Values)
386+
{
387+
world.Detach();
388+
}
389+
390+
ExtensionWorlds.Clear();
391+
}
392+
375393
/// <inheritdoc />
376394
protected internal override DeviceRequestPromptManager GetDeviceRequestPromptManager()
377395
=> FrameManager.GetDeviceRequestPromptManager(CdpCDPSession);

lib/PuppeteerSharp/Cdp/CdpPage.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,10 @@ await PrimaryTargetClient.Connection
807807
}
808808
}
809809

810+
/// <inheritdoc/>
811+
public override IReadOnlyList<Realm> ExtensionRealms()
812+
=> ((Frame)MainFrame).ExtensionRealms();
813+
810814
internal static async Task<Page> CreateAsync(
811815
CdpCDPSession client,
812816
CdpTarget target,

0 commit comments

Comments
 (0)