Skip to content

Commit 6b3aa50

Browse files
authored
Bidi: BrowserContext (#3018)
* Bidi: BrowserContext * it is ignore upstream * fix json
1 parent 76533e2 commit 6b3aa50

File tree

7 files changed

+173
-64
lines changed

7 files changed

+173
-64
lines changed

lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -337,21 +337,6 @@
337337
"FAIL"
338338
]
339339
},
340-
{
341-
"comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one",
342-
"testIdPattern": "[browsercontext.spec] *",
343-
"platforms": [
344-
"darwin",
345-
"linux",
346-
"win32"
347-
],
348-
"parameters": [
349-
"webDriverBiDi"
350-
],
351-
"expectations": [
352-
"FAIL"
353-
]
354-
},
355340
{
356341
"comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one",
357342
"testIdPattern": "[elementhandle.spec] *",
@@ -1328,21 +1313,6 @@
13281313
"FAIL"
13291314
]
13301315
},
1331-
{
1332-
"comment": "BidiBrowserContext.CloseAsync() is not implemented - fails with NotImplementedException",
1333-
"testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts",
1334-
"platforms": [
1335-
"darwin",
1336-
"linux",
1337-
"win32"
1338-
],
1339-
"parameters": [
1340-
"webDriverBiDi"
1341-
],
1342-
"expectations": [
1343-
"SKIP"
1344-
]
1345-
},
13461316
{
13471317
"comment": "BiDi Keyboard not implemented yet - requires BidiKeyboard implementation - NEEDS KEYBOARD",
13481318
"testIdPattern": "[mouse.spec] Mouse should select the text with mouse",

lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -967,13 +967,6 @@
967967
"expectations": ["FAIL"],
968968
"comment": "In BiDi currently more events than needed are fired (because target is updated more often). We probably need to adjust the test as the behavior is not broken per se"
969969
},
970-
{
971-
"testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target",
972-
"platforms": ["darwin", "linux", "win32"],
973-
"parameters": ["cdp", "firefox"],
974-
"expectations": ["SKIP"],
975-
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
976-
},
977970
{
978971
"testIdPattern": "[browsercontext.spec] BrowserContext should work across sessions",
979972
"platforms": ["darwin", "linux", "win32"],

lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextTests.cs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,30 @@ public class BrowserContextTests : PuppeteerBrowserBaseTest
1111
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should have default context")]
1212
public void ShouldHaveDefaultContext()
1313
{
14-
Assert.That(Browser.BrowserContexts(), Has.Exactly(1).Items);
15-
var defaultContext = Browser.BrowserContexts()[0];
14+
// Firefox with BiDi returns multiple user contexts (container tabs) by default
15+
Assert.That(Browser.BrowserContexts().Length, Is.GreaterThanOrEqualTo(1));
16+
}
17+
18+
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should not be able to close default context")]
19+
public void ShouldNotBeAbleToCloseDefaultContext()
20+
{
21+
var defaultContext = Browser.DefaultContext;
22+
Assert.That(defaultContext, Is.Not.Null);
1623
var exception = Assert.ThrowsAsync<PuppeteerException>(defaultContext.CloseAsync);
17-
Assert.That(Browser.DefaultContext, Is.SameAs(defaultContext));
1824
Assert.That(exception!.Message, Does.Contain("cannot be closed"));
1925
}
2026

21-
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should create new incognito context")]
22-
public async Task ShouldCreateNewIncognitoContext()
27+
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should create new context")]
28+
public async Task ShouldCreateNewContext()
2329
{
24-
Assert.That(Browser.BrowserContexts(), Has.Exactly(1).Items);
30+
var contextCount = Browser.BrowserContexts().Length;
31+
Assert.That(contextCount, Is.GreaterThanOrEqualTo(1));
2532
var context = await Browser.CreateBrowserContextAsync();
26-
Assert.That(Browser.BrowserContexts(), Has.Length.EqualTo(2));
33+
Assert.That(Browser.BrowserContexts(), Has.Length.EqualTo(contextCount + 1));
2734
Assert.That(Browser.BrowserContexts(), Does.Contain(context));
2835
await context.CloseAsync();
2936
Assert.That(context.IsClosed, Is.True);
30-
Assert.That(Browser.BrowserContexts(), Has.Exactly(1).Items);
37+
Assert.That(Browser.BrowserContexts(), Has.Length.EqualTo(contextCount));
3138
}
3239

3340
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should close all belonging targets once closing context")]
@@ -86,6 +93,9 @@ public async Task ShouldFireTargetEvents()
8693
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should isolate localStorage and cookies")]
8794
public async Task ShouldIsolateLocalStorageAndCookies()
8895
{
96+
// Firefox with BiDi returns multiple user contexts (container tabs) by default
97+
var contextCount = Browser.BrowserContexts().Length;
98+
8999
// Create two incognito contexts.
90100
var context1 = await Browser.CreateBrowserContextAsync();
91101
var context2 = await Browser.CreateBrowserContextAsync();
@@ -126,7 +136,7 @@ await page2.EvaluateExpressionAsync(@"{
126136
await Task.WhenAll(context1.CloseAsync(), context2.CloseAsync());
127137
Assert.That(context1.IsClosed, Is.True);
128138
Assert.That(context2.IsClosed, Is.True);
129-
Assert.That(Browser.BrowserContexts(), Has.Exactly(1).Items);
139+
Assert.That(Browser.BrowserContexts(), Has.Length.EqualTo(contextCount));
130140
}
131141

132142
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should work across sessions")]
@@ -149,12 +159,13 @@ public async Task ShouldWorkAcrossSessions()
149159
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should provide a context id")]
150160
public async Task ShouldProvideAContextId()
151161
{
152-
Assert.That(Browser.BrowserContexts(), Has.Exactly(1).Items);
153-
Assert.That(Browser.BrowserContexts()[0].Id, Is.Null);
162+
// Firefox with BiDi returns multiple user contexts (container tabs) by default
163+
var contextCount = Browser.BrowserContexts().Length;
164+
Assert.That(contextCount, Is.GreaterThanOrEqualTo(1));
154165

155166
await using var context = await Browser.CreateBrowserContextAsync();
156-
Assert.That(Browser.BrowserContexts(), Has.Length.EqualTo(2));
157-
Assert.That(Browser.BrowserContexts()[1].Id, Is.Not.Null);
167+
Assert.That(Browser.BrowserContexts(), Has.Length.EqualTo(contextCount + 1));
168+
Assert.That(context.Id, Is.Not.Null);
158169
}
159170

160171
[Test, PuppeteerTest("browsercontext.spec", "BrowserContext", "should wait for a target")]

lib/PuppeteerSharp/Bidi/BidiBrowser.cs

Lines changed: 14 additions & 6 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.Diagnostics.CodeAnalysis;
2526
using System.Linq;
2627
using System.Threading.Tasks;
@@ -38,7 +39,7 @@ namespace PuppeteerSharp.Bidi;
3839
public class BidiBrowser : Browser
3940
{
4041
private readonly LaunchOptions _options;
41-
private readonly ConcurrentSet<BidiBrowserContext> _browserContexts = [];
42+
private readonly ConcurrentDictionary<UserContext, BidiBrowserContext> _browserContexts = new();
4243
private readonly ILogger<BidiBrowser> _logger;
4344
private readonly BidiBrowserTarget _target;
4445
private bool _isClosed;
@@ -59,7 +60,7 @@ private BidiBrowser(Core.Browser browserCore, LaunchOptions options, ILoggerFact
5960
public override ITarget Target => _target;
6061

6162
/// <inheritdoc/>
62-
public override IBrowserContext DefaultContext => _browserContexts.FirstOrDefault(b => b.Id == BrowserCore.DefaultUserContext.Id);
63+
public override IBrowserContext DefaultContext => _browserContexts.TryGetValue(BrowserCore.DefaultUserContext, out var context) ? context : null;
6364

6465
internal static string[] SubscribeModules { get; } =
6566
[
@@ -134,7 +135,7 @@ public override ITarget[] Targets()
134135
=>
135136
[
136137
_target,
137-
.. _browserContexts.SelectMany(context => context.Targets()).ToArray()
138+
.. BrowserContexts().SelectMany(context => ((BidiBrowserContext)context).Targets()).ToArray()
138139
];
139140

140141
/// <inheritdoc />
@@ -145,7 +146,12 @@ public override async Task<IBrowserContext> CreateBrowserContextAsync(BrowserCon
145146
}
146147

147148
/// <inheritdoc />
148-
public override IBrowserContext[] BrowserContexts() => _browserContexts.ToArray();
149+
public override IBrowserContext[] BrowserContexts() =>
150+
BrowserCore.UserContexts
151+
.Select(userContext => _browserContexts.TryGetValue(userContext, out var context) ? context : null)
152+
.Where(context => context != null)
153+
.Cast<IBrowserContext>()
154+
.ToArray();
149155

150156
[SuppressMessage(
151157
"Reliability",
@@ -193,9 +199,10 @@ private void InitializeAsync()
193199

194200
private void Detach()
195201
{
196-
foreach (var context in _browserContexts)
202+
foreach (var context in _browserContexts.Values)
197203
{
198204
context.TargetCreated -= (sender, args) => OnTargetCreated(args);
205+
context.TargetChanged -= (sender, args) => OnTargetChanged(args);
199206
context.TargetDestroyed -= (sender, args) => OnTargetDestroyed(args);
200207
}
201208
}
@@ -207,9 +214,10 @@ private BidiBrowserContext CreateBrowserContext(UserContext userContext)
207214
userContext,
208215
new BidiBrowserContextOptions() { DefaultViewport = _options.DefaultViewport, });
209216

210-
_browserContexts.Add(browserContext);
217+
_browserContexts.TryAdd(userContext, browserContext);
211218

212219
browserContext.TargetCreated += (sender, args) => OnTargetCreated(args);
220+
browserContext.TargetChanged += (sender, args) => OnTargetChanged(args);
213221
browserContext.TargetDestroyed += (sender, args) => OnTargetDestroyed(args);
214222

215223
return browserContext;

lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121
// * SOFTWARE.
2222

23+
using System;
2324
using System.Collections.Concurrent;
2425
using System.Collections.Generic;
2526
using System.Linq;
2627
using System.Threading.Tasks;
28+
using Microsoft.Extensions.Logging;
2729
using PuppeteerSharp.Bidi.Core;
2830
using PuppeteerSharp.Helpers;
31+
using WebDriverBiDi.Permissions;
2932

3033
namespace PuppeteerSharp.Bidi;
3134

@@ -34,6 +37,7 @@ public class BidiBrowserContext : BrowserContext
3437
{
3538
private readonly ConcurrentDictionary<BrowsingContext, BidiPage> _pages = [];
3639
private readonly ConcurrentDictionary<BidiPage, BidiPageTargetInfo> _targets = new();
40+
private readonly List<(string Origin, OverridePermission Permission)> _overrides = [];
3741

3842
private BidiBrowserContext(BidiBrowser browser, UserContext userContext, BidiBrowserContextOptions options)
3943
{
@@ -49,10 +53,69 @@ private BidiBrowserContext(BidiBrowser browser, UserContext userContext, BidiBro
4953
internal UserContext UserContext { get; }
5054

5155
/// <inheritdoc />
52-
public override Task OverridePermissionsAsync(string origin, IEnumerable<OverridePermission> permissions) => throw new System.NotImplementedException();
56+
public override async Task OverridePermissionsAsync(string origin, IEnumerable<OverridePermission> permissions)
57+
{
58+
var permissionsSet = new HashSet<OverridePermission>(permissions);
59+
60+
// We need to set all permissions - grant the ones in the list, deny the rest
61+
var tasks = new List<Task>();
62+
foreach (OverridePermission permission in Enum.GetValues(typeof(OverridePermission)))
63+
{
64+
var state = permissionsSet.Contains(permission)
65+
? PermissionState.Granted
66+
: PermissionState.Denied;
67+
68+
var permissionName = GetPermissionName(permission);
69+
70+
var task = UserContext.SetPermissionsAsync(origin, permissionName, state);
71+
_overrides.Add((origin, permission));
72+
73+
// Denying some outdated permissions might fail, so we catch those errors
74+
if (!permissionsSet.Contains(permission))
75+
{
76+
task = task.ContinueWith(
77+
t =>
78+
{
79+
if (t.IsFaulted)
80+
{
81+
// Log the error but don't throw
82+
((BidiBrowser)Browser).LoggerFactory?.CreateLogger<BidiBrowserContext>()
83+
.LogDebug(t.Exception, "Failed to deny permission {Permission}", permission);
84+
}
85+
},
86+
TaskScheduler.Default);
87+
}
88+
89+
tasks.Add(task);
90+
}
91+
92+
await Task.WhenAll(tasks).ConfigureAwait(false);
93+
}
5394

5495
/// <inheritdoc />
55-
public override Task ClearPermissionOverridesAsync() => throw new System.NotImplementedException();
96+
public override async Task ClearPermissionOverridesAsync()
97+
{
98+
var tasks = new List<Task>();
99+
foreach (var (origin, permission) in _overrides.ToArray())
100+
{
101+
var permissionName = GetPermissionName(permission);
102+
tasks.Add(UserContext.SetPermissionsAsync(origin, permissionName, PermissionState.Prompt)
103+
.ContinueWith(
104+
t =>
105+
{
106+
if (t.IsFaulted)
107+
{
108+
// Log the error but don't throw
109+
((BidiBrowser)Browser).LoggerFactory?.CreateLogger<BidiBrowserContext>()
110+
.LogDebug(t.Exception, "Failed to reset permission {Permission}", permission);
111+
}
112+
},
113+
TaskScheduler.Default));
114+
}
115+
116+
_overrides.Clear();
117+
await Task.WhenAll(tasks).ConfigureAwait(false);
118+
}
56119

57120
/// <inheritdoc />
58121
public override Task<IPage[]> PagesAsync() => Task.FromResult(_pages.Values.Cast<IPage>().ToArray());
@@ -83,7 +146,25 @@ public override async Task<IPage> NewPageAsync()
83146
}
84147

85148
/// <inheritdoc />
86-
public override Task CloseAsync() => throw new System.NotImplementedException();
149+
public override async Task CloseAsync()
150+
{
151+
if (UserContext.Id == UserContext.DEFAULT)
152+
{
153+
throw new PuppeteerException("Default BrowserContext cannot be closed!");
154+
}
155+
156+
try
157+
{
158+
await UserContext.RemoveAsync().ConfigureAwait(false);
159+
}
160+
catch (Exception ex)
161+
{
162+
((BidiBrowser)Browser).LoggerFactory?.CreateLogger<BidiBrowserContext>()
163+
.LogDebug(ex, "Failed to close browser context");
164+
}
165+
166+
_targets.Clear();
167+
}
87168

88169
/// <inheritdoc />
89170
public override ITarget[] Targets()
@@ -107,6 +188,28 @@ internal static BidiBrowserContext From(
107188
return context;
108189
}
109190

191+
private static string GetPermissionName(OverridePermission permission)
192+
{
193+
return permission switch
194+
{
195+
OverridePermission.Geolocation => "geolocation",
196+
OverridePermission.Midi => "midi",
197+
OverridePermission.Notifications => "notifications",
198+
OverridePermission.Camera => "camera",
199+
OverridePermission.Microphone => "microphone",
200+
OverridePermission.BackgroundSync => "background-sync",
201+
OverridePermission.Sensors => "accelerometer",
202+
OverridePermission.AccessibilityEvents => "accessibility-events",
203+
OverridePermission.ClipboardReadWrite => "clipboard-read",
204+
OverridePermission.PaymentHandler => "payment-handler",
205+
OverridePermission.MidiSysex => "midi-sysex",
206+
OverridePermission.IdleDetection => "idle-detection",
207+
OverridePermission.PersistentStorage => "persistent-storage",
208+
OverridePermission.LocalNetworkAccess => "local-network-access",
209+
_ => throw new ArgumentOutOfRangeException(nameof(permission), permission, "Unknown permission"),
210+
};
211+
}
212+
110213
private void Initialize()
111214
{
112215
// Create targets for existing browsing contexts.

lib/PuppeteerSharp/Bidi/BidiPageTarget.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,15 @@ internal class BidiPageTarget(BidiPage page) : Target
3333

3434
public override ITarget Opener => throw new InvalidOperationException();
3535

36-
internal override Browser Browser { get; }
36+
internal override Browser Browser => (Browser)page.BrowserContext.Browser;
3737

38-
internal override BrowserContext BrowserContext => BidiBrowserContext;
39-
40-
internal BidiBrowserContext BidiBrowserContext { get; }
38+
internal override BrowserContext BrowserContext => (BrowserContext)page.BrowserContext;
4139

4240
public override Task<IPage> PageAsync() => Task.FromResult<IPage>(page);
4341

4442
public override Task<IPage> AsPageAsync()
4543
#pragma warning disable CA2000
46-
=> Task.FromResult(BidiPage.From(BidiBrowserContext, page.BidiMainFrame.BrowsingContext) as IPage);
44+
=> Task.FromResult(BidiPage.From((BidiBrowserContext)page.BrowserContext, page.BidiMainFrame.BrowsingContext) as IPage);
4745
#pragma warning restore CA2000
4846

4947
public override Task<ICDPSession> CreateCDPSessionAsync()

0 commit comments

Comments
 (0)