diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3c512398f..a62419d0d 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ env: jobs: build: - name: build-${{ matrix.browser }}-${{ matrix.mode }}-${{ matrix.os }} + name: ${{ matrix.browser }}-${{ matrix.mode }}-${{ matrix.os }}-${{ matrix.protocol }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -28,33 +28,31 @@ jobs: - os: ubuntu-latest browser: CHROME mode: headless + protocol: cdp - os: ubuntu-latest browser: CHROME mode: headful + protocol: cdp - os: ubuntu-latest browser: CHROME mode: headless-shell - - os: ubuntu-latest - browser: FIREFOX - mode: headless - - os: ubuntu-latest - browser: FIREFOX - mode: headful + protocol: cdp - os: windows-latest browser: CHROME mode: headless + protocol: cdp - os: windows-latest browser: CHROME mode: headful + protocol: cdp - os: windows-latest browser: CHROME mode: headless-shell - - os: windows-latest + protocol: cdp + - os: ubuntu-latest browser: FIREFOX mode: headless - - os: windows-latest - browser: FIREFOX - mode: headful + protocol: webdriverbidi steps: - uses: actions/checkout@v3 - name: Setup .NET Core @@ -82,7 +80,7 @@ jobs: New-SelfSignedCertificate -Subject "localhost" -FriendlyName "Puppeteer" -CertStoreLocation "cert:\CurrentUser\My" Get-ChildItem -Path cert:\CurrentUSer\my | where { $_.friendlyname -eq "Puppeteer" } | Export-Certificate -FilePath $env:GITHUB_WORKSPACE\lib\PuppeteerSharp.TestServer\testCert.cer - name: Check formatting - if: ${{ matrix.os == 'ubuntu-latest' && matrix.browser == 'CHROME' && matrix.mode == 'headless' }} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.browser == 'CHROME' && matrix.mode == 'headless' && matrix.protocol == 'cdp' }} run: dotnet format ./lib/PuppeteerSharp.sln --verify-no-changes --exclude-diagnostics CA1865 - name: Build working-directory: lib @@ -95,6 +93,7 @@ jobs: env: BROWSER: ${{ matrix.browser }} HEADLESS_MODE: ${{ matrix.mode }} + PROTOCOL: ${{ matrix.protocol }} run: | Xvfb :1 -screen 5 1024x768x8 & export DISPLAY=:1.5 @@ -105,6 +104,7 @@ jobs: env: BROWSER: ${{ matrix.browser }} HEADLESS_MODE: ${{ matrix.mode }} + PROTOCOL: ${{ matrix.protocol }} run: | cd .\lib\PuppeteerSharp.Tests dotnet test -s test.runsettings --blame-hang-timeout 300000 diff --git a/lib/PuppeteerSharp.Nunit/PuppeteerTestAttribute.cs b/lib/PuppeteerSharp.Nunit/PuppeteerTestAttribute.cs index 6b4d1363f..c8bc6d49c 100644 --- a/lib/PuppeteerSharp.Nunit/PuppeteerTestAttribute.cs +++ b/lib/PuppeteerSharp.Nunit/PuppeteerTestAttribute.cs @@ -21,9 +21,10 @@ public class PuppeteerTestAttribute : NUnitAttribute, IApplyToTest { private static TestExpectation[] _localExpectations; private static TestExpectation[] _upstreamExpectations; + public static readonly bool IsChrome = Environment.GetEnvironmentVariable("BROWSER") != "FIREFOX"; - // TODO: Change implementation when we implement Webdriver Bidi - public static readonly bool IsCdp = true; + public static readonly bool IsCdp = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("PROTOCOL")) || Environment.GetEnvironmentVariable("PROTOCOL")!.Equals("cdp"); + public static readonly HeadlessMode Headless = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HEADLESS_MODE")) ? (System.Diagnostics.Debugger.IsAttached ? HeadlessMode.False : HeadlessMode.True) : diff --git a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json index 51c349d4c..1c26431ec 100644 --- a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json +++ b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json @@ -85,7 +85,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[accessibility.spec] *", + "testIdPattern": "[browser.spec] *", "platforms": [ "darwin", "linux", @@ -100,7 +100,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[ariaqueryhandler.spec] *", + "testIdPattern": "[navigation.spec] *should navigate to about:blank*", "platforms": [ "darwin", "linux", @@ -115,7 +115,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[autofill.spec] *", + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation*", "platforms": [ "darwin", "linux", @@ -130,7 +130,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[browser.spec] *", + "testIdPattern": "[navigation.spec] *should work with anchor navigation*", "platforms": [ "darwin", "linux", @@ -145,7 +145,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[browsercontext.spec] *", + "testIdPattern": "[navigation.spec] * should fail when main resources failed to load", "platforms": [ "darwin", "linux", @@ -160,7 +160,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[cdp] *", + "testIdPattern": "[navigation.spec] navigation Frame.goto*", "platforms": [ "darwin", "linux", @@ -175,7 +175,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[chromiumonly.spec] *", + "testIdPattern": "[navigation.spec] navigation Page.reload*", "platforms": [ "darwin", "linux", @@ -190,7 +190,307 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[click.spec] *", + "testIdPattern": "[navigation.spec] *should fail when navigating and show the url at the error message*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should work with self requesting page*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should work when reload causes history API in beforeunload*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should work when navigating to valid url*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should work when navigating to 404*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should wait for network idle to succeed navigation*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should return last response in redirect chain*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should not throw an error for a 404 response with an empty body*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should navigate to page with iframe and networkidle0*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should navigate to empty page with networkidle2*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should navigate to empty page with networkidle0*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should navigate to empty page with domcontentloaded*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should not throw an error for a 500 response with an empty body*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should return response when page changes its URL after load*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *SSL*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *should fail*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] navigation Page.goBack *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] *timeout*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation*", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[ariaqueryhandler.spec] *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[chromiumonly.spec] *", "platforms": [ "darwin", "linux", @@ -595,7 +895,7 @@ }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[navigation.spec] *", + "testIdPattern": "[click.spec] *", "platforms": [ "darwin", "linux", @@ -847,5 +1147,110 @@ "expectations": [ "FAIL" ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[ChromeLauncher.test.ts] *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[NetworkManager.test.ts] *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[DeviceRequestPrompt.test.ts] *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[launcher.spec] PuppeteerSharp *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[puppeteer-sharp.spec.ts] PuppeteerSharp *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[elementhandle.spec] PuppeteerSharp *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] + }, + { + "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", + "testIdPattern": "[prerender.spec] *", + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "webDriverBiDi" + ], + "expectations": [ + "FAIL" + ] } ] diff --git a/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextTests.cs b/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextTests.cs index 8cc7bd047..12664db51 100644 --- a/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextTests.cs +++ b/lib/PuppeteerSharp.Tests/BrowserContextTests/BrowserContextTests.cs @@ -138,7 +138,8 @@ public async Task ShouldWorkAcrossSessions() var remoteBrowser = await Puppeteer.ConnectAsync(new ConnectOptions { - BrowserWSEndpoint = Browser.WebSocketEndpoint + BrowserWSEndpoint = Browser.WebSocketEndpoint, + Protocol = ((Browser)Browser).Protocol, }); var contexts = remoteBrowser.BrowserContexts(); Assert.That(contexts, Has.Length.EqualTo(2)); diff --git a/lib/PuppeteerSharp.Tests/BrowserTests/IsConnectedTests.cs b/lib/PuppeteerSharp.Tests/BrowserTests/IsConnectedTests.cs index 6aa9c73af..b45bf94db 100644 --- a/lib/PuppeteerSharp.Tests/BrowserTests/IsConnectedTests.cs +++ b/lib/PuppeteerSharp.Tests/BrowserTests/IsConnectedTests.cs @@ -6,16 +6,13 @@ namespace PuppeteerSharp.Tests.BrowserTests { public class IsConnectedTests : PuppeteerBrowserBaseTest { - public IsConnectedTests() : base() - { - } - [Test, Retry(2), PuppeteerTest("browser.spec", "Browser.isConnected", "should set the browser connected state")] public async Task ShouldSetTheBrowserConnectedState() { var newBrowser = await Puppeteer.ConnectAsync(new ConnectOptions { - BrowserWSEndpoint = Browser.WebSocketEndpoint + BrowserWSEndpoint = Browser.WebSocketEndpoint, + Protocol = ((Browser)Browser).Protocol, }); Assert.That(newBrowser.IsConnected, Is.True); newBrowser.Disconnect(); diff --git a/lib/PuppeteerSharp.Tests/BrowserTests/ProcessTests.cs b/lib/PuppeteerSharp.Tests/BrowserTests/ProcessTests.cs index 16c16d7ad..0c7c25cee 100644 --- a/lib/PuppeteerSharp.Tests/BrowserTests/ProcessTests.cs +++ b/lib/PuppeteerSharp.Tests/BrowserTests/ProcessTests.cs @@ -6,8 +6,6 @@ namespace PuppeteerSharp.Tests.BrowserTests { public class ProcessTests : PuppeteerBrowserBaseTest { - public ProcessTests() : base() { } - [Test, Retry(2), PuppeteerTest("browser.spec", "Browser.process", "should return child_process instance")] public void ShouldReturnProcessInstance() { @@ -20,8 +18,14 @@ public async Task ShouldNotReturnChildProcessForRemoteBrowser() { var browserWSEndpoint = Browser.WebSocketEndpoint; var remoteBrowser = await Puppeteer.ConnectAsync( - new ConnectOptions { BrowserWSEndpoint = browserWSEndpoint }, TestConstants.LoggerFactory); + new ConnectOptions + { + BrowserWSEndpoint = browserWSEndpoint, + Protocol = ((Browser)Browser).Protocol, + }, + TestConstants.LoggerFactory); Assert.That(remoteBrowser.Process, Is.Null); + remoteBrowser.Disconnect(); } } diff --git a/lib/PuppeteerSharp.Tests/CookiesTests/SetCookiesTests.cs b/lib/PuppeteerSharp.Tests/CookiesTests/SetCookiesTests.cs index c7fa517fa..578739e3d 100644 --- a/lib/PuppeteerSharp.Tests/CookiesTests/SetCookiesTests.cs +++ b/lib/PuppeteerSharp.Tests/CookiesTests/SetCookiesTests.cs @@ -7,10 +7,6 @@ namespace PuppeteerSharp.Tests.CookiesTests { public class SetCookiesTests : PuppeteerPageBaseTest { - public SetCookiesTests() : base() - { - } - [Test, Retry(2), PuppeteerTest("cookies.spec", "Cookie specs Page.setCookie", "should work")] public async Task ShouldWork() { diff --git a/lib/PuppeteerSharp.Tests/ElementHandleTests/ContentFrameTests.cs b/lib/PuppeteerSharp.Tests/ElementHandleTests/ContentFrameTests.cs index 9fc36a2c6..26002f913 100644 --- a/lib/PuppeteerSharp.Tests/ElementHandleTests/ContentFrameTests.cs +++ b/lib/PuppeteerSharp.Tests/ElementHandleTests/ContentFrameTests.cs @@ -24,7 +24,7 @@ public async Task ShouldWork() Assert.That(frame, Is.EqualTo(Page.FirstChildFrame())); } - [Test] + [Test, Retry(2), PuppeteerTest("elementhandle.spec", "PuppeteerSharp", "should work headful")] public async Task ShouldWorkHeadful() { await using var Browser = await Puppeteer.LaunchAsync(_headfulOptions); diff --git a/lib/PuppeteerSharp.Tests/LauncherTests/BrowserCloseTests.cs b/lib/PuppeteerSharp.Tests/LauncherTests/BrowserCloseTests.cs index 2b77b0922..0a2b363d9 100644 --- a/lib/PuppeteerSharp.Tests/LauncherTests/BrowserCloseTests.cs +++ b/lib/PuppeteerSharp.Tests/LauncherTests/BrowserCloseTests.cs @@ -11,7 +11,12 @@ public class BrowserCloseTests : PuppeteerBrowserBaseTest public async Task ShouldTerminateNetworkWaiters() { await using var browser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions()); - await using var remote = await Puppeteer.ConnectAsync(new ConnectOptions { BrowserWSEndpoint = browser.WebSocketEndpoint }); + await using var remote = await Puppeteer.ConnectAsync(new ConnectOptions + { + BrowserWSEndpoint = browser.WebSocketEndpoint, + Protocol = ((Browser)browser).Protocol, + }); + var newPage = await remote.NewPageAsync(); var requestTask = newPage.WaitForRequestAsync(TestConstants.EmptyPage); var responseTask = newPage.WaitForResponseAsync(TestConstants.EmptyPage); @@ -27,7 +32,7 @@ public async Task ShouldTerminateNetworkWaiters() Assert.That(exception.Message, Does.Not.Contain("Timeout")); } - [Test] + [Test, Retry(2), PuppeteerTest("launcher.spec", "PuppeteerSharp", "delete temp user data dir when disposing browser")] public async Task DeleteTempUserDataDirWhenDisposingBrowser() { var options = TestConstants.DefaultBrowserOptions(); diff --git a/lib/PuppeteerSharp.Tests/LauncherTests/BrowserDisconnectTests.cs b/lib/PuppeteerSharp.Tests/LauncherTests/BrowserDisconnectTests.cs index e27d5d43a..b5ed6a0ec 100644 --- a/lib/PuppeteerSharp.Tests/LauncherTests/BrowserDisconnectTests.cs +++ b/lib/PuppeteerSharp.Tests/LauncherTests/BrowserDisconnectTests.cs @@ -15,7 +15,8 @@ public async Task ShouldRejectNavigationWhenBrowserCloses() await using var browser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions()); var remote = await Puppeteer.ConnectAsync(new ConnectOptions { - BrowserWSEndpoint = browser.WebSocketEndpoint + BrowserWSEndpoint = browser.WebSocketEndpoint, + Protocol = ((Browser)Browser).Protocol, }); var page = await remote.NewPageAsync(); var navigationTask = page.GoToAsync(TestConstants.ServerUrl + "/one-style.html", new NavigationOptions @@ -41,7 +42,8 @@ public async Task ShouldRejectWaitForSelectorWhenBrowserCloses() await using var browser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions()); var remote = await Puppeteer.ConnectAsync(new ConnectOptions { - BrowserWSEndpoint = browser.WebSocketEndpoint + BrowserWSEndpoint = browser.WebSocketEndpoint, + Protocol = ((Browser)browser).Protocol, }); var page = await remote.NewPageAsync(); var watchdog = page.WaitForSelectorAsync("div", new WaitForSelectorOptions { Timeout = 60000 }); diff --git a/lib/PuppeteerSharp.Tests/LauncherTests/BrowserEventsDisconnectedTests.cs b/lib/PuppeteerSharp.Tests/LauncherTests/BrowserEventsDisconnectedTests.cs index 6ef713baa..961771c6b 100644 --- a/lib/PuppeteerSharp.Tests/LauncherTests/BrowserEventsDisconnectedTests.cs +++ b/lib/PuppeteerSharp.Tests/LauncherTests/BrowserEventsDisconnectedTests.cs @@ -14,7 +14,11 @@ public BrowserEventsDisconnectedTests() : base() public async Task ShouldEmittedWhenBrowserGetsClosedDisconnectedOrUnderlyingWebsocketGetsClosed() { var originalBrowser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions(), TestConstants.LoggerFactory); - var connectOptions = new ConnectOptions { BrowserWSEndpoint = originalBrowser.WebSocketEndpoint }; + var connectOptions = new ConnectOptions + { + BrowserWSEndpoint = originalBrowser.WebSocketEndpoint, + Protocol = ((Browser)Browser).Protocol, + }; var remoteBrowser1 = await Puppeteer.ConnectAsync(connectOptions, TestConstants.LoggerFactory); var remoteBrowser2 = await Puppeteer.ConnectAsync(connectOptions, TestConstants.LoggerFactory); diff --git a/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerConnectTests.cs b/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerConnectTests.cs index 4ee159efa..977a1741c 100644 --- a/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerConnectTests.cs +++ b/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerConnectTests.cs @@ -15,7 +15,8 @@ public async Task ShouldBeAbleToConnectMultipleTimesToSameBrowser() { var options = new ConnectOptions() { - BrowserWSEndpoint = Browser.WebSocketEndpoint + BrowserWSEndpoint = Browser.WebSocketEndpoint, + Protocol = ((Browser)Browser).Protocol, }; var browser = await Puppeteer.ConnectAsync(options, TestConstants.LoggerFactory); await using (var page = await browser.NewPageAsync()) @@ -54,7 +55,8 @@ public async Task ShouldSupportAcceptInsecureCertsOption() await using var browser = await Puppeteer.ConnectAsync(new ConnectOptions { BrowserWSEndpoint = originalBrowser.WebSocketEndpoint, - AcceptInsecureCerts = true + AcceptInsecureCerts = true, + Protocol = ((Browser)Browser).Protocol, }); await using var page = await browser.NewPageAsync(); var requestTask = HttpsServer.WaitForRequest( @@ -87,6 +89,7 @@ public async Task ShouldSupportTargetFilter() { BrowserWSEndpoint = browser.WebSocketEndpoint, TargetFilter = target => !target.Url.Contains("should-be-ignored"), + Protocol = ((Browser)browser).Protocol, }, TestConstants.LoggerFactory); var pages = await remoteBrowser.PagesAsync(); @@ -129,7 +132,8 @@ public async Task ShouldBeAbleToReconnectToADisconnectedBrowser() { var options = new ConnectOptions() { - BrowserWSEndpoint = Browser.WebSocketEndpoint + BrowserWSEndpoint = Browser.WebSocketEndpoint, + Protocol = ((Browser)Browser).Protocol, }; var url = TestConstants.ServerUrl + "/frames/nested-frames.html"; @@ -154,7 +158,8 @@ public async Task ShouldBeAbleToConnectToTheSamePageSimultaneously() var browserOne = await Puppeteer.LaunchAsync(new LaunchOptions()); var browserTwo = await Puppeteer.ConnectAsync(new ConnectOptions { - BrowserWSEndpoint = browserOne.WebSocketEndpoint + BrowserWSEndpoint = browserOne.WebSocketEndpoint, + Protocol = ((Browser)browserOne).Protocol, }); var tcs = new TaskCompletionSource(); async void TargetCreated(object sender, TargetChangedArgs e) @@ -185,7 +190,8 @@ public async Task ShouldBeAbleToReconnect() var browserTwo = await Puppeteer.ConnectAsync(new ConnectOptions { - BrowserWSEndpoint = browserWSEndpoint + BrowserWSEndpoint = browserWSEndpoint, + Protocol = ((Browser)browserOne).Protocol, }); var pages = await browserTwo.PagesAsync(); diff --git a/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerLaunchTests.cs b/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerLaunchTests.cs index 65332b3a8..ab9cb4ac5 100644 --- a/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerLaunchTests.cs +++ b/lib/PuppeteerSharp.Tests/LauncherTests/PuppeteerLaunchTests.cs @@ -14,7 +14,7 @@ namespace PuppeteerSharp.Tests.LauncherTests { public class PuppeteerLaunchTests : PuppeteerBaseTest { - [Test, Retry(2)] + [Test, Retry(2), PuppeteerTest("launcher.spec", "PuppeteerSharp", "should work in real life")] public async Task ShouldWorkInRealLife() { var options = TestConstants.DefaultBrowserOptions(); @@ -187,6 +187,7 @@ public void ShouldReturnTheDefaultArguments() [TestCase(false)] [TestCase(true)] + [PuppeteerTest("launcher.spec", "PuppeteerSharp", "chrome should be closed")] public async Task ChromeShouldBeClosed(bool useDisposeAsync) { var options = TestConstants.DefaultBrowserOptions(); @@ -339,7 +340,7 @@ public async Task ShouldTakeFullPageScreenshotsWhenDefaultViewportIsNull() Assert.That(await page.ScreenshotDataAsync(new ScreenshotOptions { FullPage = true }), Is.Not.Empty); } - [Test, Retry(2)] + [Test, Retry(2), PuppeteerTest("launcher.spec", "PuppeteerSharp", "should support custom websocket")] public async Task ShouldSupportCustomWebSocket() { var options = TestConstants.DefaultBrowserOptions(); @@ -356,7 +357,7 @@ public async Task ShouldSupportCustomWebSocket() } } - [Test, Retry(2)] + [Test, Retry(2), PuppeteerTest("launcher.spec", "Launcher specs Browser.close", "should support custom transport")] public async Task ShouldSupportCustomTransport() { var customTransportCreated = false; diff --git a/lib/PuppeteerSharp.Tests/NavigationTests/PageWaitForNavigationTests.cs b/lib/PuppeteerSharp.Tests/NavigationTests/PageWaitForNavigationTests.cs index ad6532cf5..a8c90d616 100644 --- a/lib/PuppeteerSharp.Tests/NavigationTests/PageWaitForNavigationTests.cs +++ b/lib/PuppeteerSharp.Tests/NavigationTests/PageWaitForNavigationTests.cs @@ -26,7 +26,7 @@ await Task.WhenAll( Assert.That(response.Url, Does.Contain("grid.html")); } - [Test, Retry(2), PuppeteerTest("navigation.spec", "Page.waitForNavigation", "should work with both domcontentloaded and load")] + [Test, Retry(2), PuppeteerTest("navigation.spec", "navigation Page.waitForNavigation", "should work with both domcontentloaded and load")] public async Task ShouldWorkWithBothDomcontentloadedAndLoad() { var responseCompleted = new TaskCompletionSource(); diff --git a/lib/PuppeteerSharp.Tests/PageTests/CloseTests.cs b/lib/PuppeteerSharp.Tests/PageTests/CloseTests.cs index c3d6aac7b..bf1223a36 100644 --- a/lib/PuppeteerSharp.Tests/PageTests/CloseTests.cs +++ b/lib/PuppeteerSharp.Tests/PageTests/CloseTests.cs @@ -128,7 +128,7 @@ public async Task ShouldTerminateNetworkWaiters() Assert.That(exception.Message, Does.Not.Contain("Timeout")); } - [Test, Retry(2)] + [Test, Retry(2), PuppeteerTest("page.spec", "Page Page.close PuppeteerSharp", "should close when connection breaks prematurely")] public async Task ShouldCloseWhenConnectionBreaksPrematurely() { Browser.Disconnect(); diff --git a/lib/PuppeteerSharp.Tests/PageTests/GetContentTests.cs b/lib/PuppeteerSharp.Tests/PageTests/GetContentTests.cs index d989e2815..fed1051a9 100644 --- a/lib/PuppeteerSharp.Tests/PageTests/GetContentTests.cs +++ b/lib/PuppeteerSharp.Tests/PageTests/GetContentTests.cs @@ -8,7 +8,7 @@ namespace PuppeteerSharp.Tests.PageTests public class GetContentTests : PuppeteerPageBaseTest { - [Test, Retry(2)] + [Test, Retry(2), PuppeteerTest("puppeteer-sharp.spec.ts", "PuppeteerSharp", "should work with lone surrogate")] public async Task ShouldWorkWithLoneSurrogate() { await Page.GoToAsync(TestConstants.ServerUrl + "/lone-surrogate.html"); diff --git a/lib/PuppeteerSharp.Tests/TestConstants.cs b/lib/PuppeteerSharp.Tests/TestConstants.cs index 9f5d5e011..bfe01bebb 100644 --- a/lib/PuppeteerSharp.Tests/TestConstants.cs +++ b/lib/PuppeteerSharp.Tests/TestConstants.cs @@ -22,7 +22,6 @@ public static class TestConstants public static readonly string EmptyPage = $"{ServerUrl}/empty.html"; public static readonly string CrossProcessUrl = ServerIpUrl; public static readonly bool IsChrome = PuppeteerTestAttribute.IsChrome; - public static readonly DeviceDescriptor IPhone = Puppeteer.Devices[DeviceDescriptorName.IPhone6]; public static readonly DeviceDescriptor IPhone6Landscape = Puppeteer.Devices[DeviceDescriptorName.IPhone6Landscape]; @@ -46,6 +45,7 @@ public static class TestConstants EnqueueAsyncMessages = Convert.ToBoolean(Environment.GetEnvironmentVariable("ENQUEUE_ASYNC_MESSAGES") ?? "false"), Timeout = 0, LogProcess = true, + Protocol = PuppeteerTestAttribute.IsCdp ? ProtocolType.Cdp : ProtocolType.WebdriverBiDi, #if NETCOREAPP EnqueueTransportMessages = false #else diff --git a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs index 7c39acb5a..e2d57aef6 100644 --- a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs +++ b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForFunctionTests.cs @@ -29,7 +29,7 @@ public FrameWaitForFunctionTests() : base() public void Dispose() { - _pollerInterceptor.Dispose(); + _pollerInterceptor?.Dispose(); } [Test, Retry(2), PuppeteerTest("waittask.spec", "waittask specs Frame.waitForFunction", "should work when resolved right before execution context disposal")] diff --git a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs index 2e6c78158..a18a6ac73 100644 --- a/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs +++ b/lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForSelectorTests.cs @@ -32,7 +32,7 @@ public FrameWaitForSelectorTests() public void Dispose() { - _pollerInterceptor.Dispose(); + _pollerInterceptor?.Dispose(); } [Test, Retry(2), PuppeteerTest("waittask.spec", "waittask specs Frame.waitForSelector", "should immediately resolve promise if node exists")] diff --git a/lib/PuppeteerSharp/Bidi/BidiBrowser.cs b/lib/PuppeteerSharp/Bidi/BidiBrowser.cs new file mode 100644 index 000000000..c2c62bd62 --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiBrowser.cs @@ -0,0 +1,193 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using PuppeteerSharp.Bidi.Core; +using PuppeteerSharp.Helpers; +using WebDriverBiDi; +using WebDriverBiDi.Session; + +namespace PuppeteerSharp.Bidi; + +/// +/// Represents a browser connected using the Bidi protocol. +/// +public class BidiBrowser : Browser +{ + private readonly LaunchOptions _options; + private readonly ConcurrentSet _browserContexts = []; + private readonly ILogger _logger; + private readonly BidiBrowserTarget _target; + + private BidiBrowser(Core.Browser browserCore, LaunchOptions options, ILoggerFactory loggerFactory) + { + _target = new BidiBrowserTarget(this); + _options = options; + BrowserCore = browserCore; + _logger = loggerFactory.CreateLogger(); + } + + /// + public override bool IsClosed { get; } + + internal static string[] SubscribeModules { get; } = + [ + "browsingContext", + "network", + "log", + "script", + ]; + + internal static string[] SubscribeCdpEvents { get; } = + [ + "cdp.Debugger.scriptParsed", + "cdp.CSS.styleSheetAdded", + "cdp.Runtime.executionContextsCleared", + "cdp.Tracing.tracingComplete", + "cdp.Network.requestWillBeSent", + "cdp.Debugger.scriptParsed", + "cdp.Page.screencastFrame", + ]; + + internal BiDiDriver Driver => BrowserCore.Session.Driver; + + internal Core.Browser BrowserCore { get; } + + internal override ProtocolType Protocol => ProtocolType.WebdriverBiDi; + + // TODO: Implement + internal bool CdpSupported => false; + + internal string BrowserVersion => BrowserCore.Session.Info.Capabilities.BrowserVersion; + + internal string BrowserName => BrowserCore.Session.Info.Capabilities.BrowserName; + + /// + public override Task GetVersionAsync() => Task.FromResult($"{BrowserName}/{BrowserVersion}"); + + /// + public override Task GetUserAgentAsync() => throw new NotImplementedException(); + + /// + public override void Disconnect() => throw new NotImplementedException(); + + /// + public override async Task CloseAsync() + { + try + { + await BrowserCore.CloseAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to close connection"); + } + finally + { + await Driver.StopAsync().ConfigureAwait(false); + } + } + + /// + public override Task NewPageAsync() => throw new NotImplementedException(); + + /// + public override ITarget[] Targets() + => + [ + _target, + .. _browserContexts.SelectMany(context => context.Targets()).ToArray() + ]; + + /// + public override async Task CreateBrowserContextAsync(BrowserContextOptions options = null) + { + var userContext = await BrowserCore.CreateUserContextAsync().ConfigureAwait(false); + return CreateBrowserContext(userContext); + } + + /// + public override IBrowserContext[] BrowserContexts() => throw new NotImplementedException(); + + [SuppressMessage( + "Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "We return the session, the browser needs to dispose the session")] + internal static async Task CreateAsync( + BiDiDriver driver, + LaunchOptions options, + ILoggerFactory loggerFactory) + { + var session = await Session.FromAsync( + driver, + new NewCommandParameters + { + Capabilities = new CapabilitiesRequest() + { + AlwaysMatch = new CapabilityRequest() + { + AcceptInsecureCertificates = options.AcceptInsecureCerts, + AdditionalCapabilities = { ["webSocketUrl"] = true, }, + }, + }, + }, + loggerFactory).ConfigureAwait(false); + + await session.SubscribeAsync( + session.Info.Capabilities.BrowserName.ToLowerInvariant().Contains("firefox") + ? SubscribeModules + : [.. SubscribeModules, .. SubscribeCdpEvents]).ConfigureAwait(false); + + var browser = new BidiBrowser(session.Browser, options, loggerFactory); + browser.InitializeAsync(); + return browser; + } + + private void InitializeAsync() + { + // Initializing existing contexts. + foreach (var userContext in BrowserCore.UserContexts) + { + CreateBrowserContext(userContext); + } + } + + private BidiBrowserContext CreateBrowserContext(UserContext userContext) + { + var browserContext = BidiBrowserContext.From( + this, + userContext, + new BidiBrowserContextOptions() { DefaultViewport = _options.DefaultViewport, }); + + _browserContexts.Add(browserContext); + + browserContext.TargetCreated += (sender, args) => OnTargetCreated(args); + browserContext.TargetDestroyed += (sender, args) => OnTargetDestroyed(args); + + return browserContext; + } +} + diff --git a/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs b/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs new file mode 100644 index 000000000..67a78b7bc --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiBrowserContext.cs @@ -0,0 +1,217 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using PuppeteerSharp.Bidi.Core; +using PuppeteerSharp.Helpers; + +namespace PuppeteerSharp.Bidi; + +/// +public class BidiBrowserContext : BrowserContext +{ + private readonly ConcurrentDictionary _pages = []; + private readonly ConcurrentDictionary _targets = new(); + + private BidiBrowserContext(BidiBrowser browser, UserContext userContext, BidiBrowserContextOptions options) + { + UserContext = userContext; + Browser = browser; + DefaultViewport = options.DefaultViewport; + } + + internal ViewPortOptions DefaultViewport { get; set; } + + internal TaskQueue ScreenshotTaskQueue => Browser.ScreenshotTaskQueue; + + internal UserContext UserContext { get; } + + /// + public override Task OverridePermissionsAsync(string origin, IEnumerable permissions) => throw new System.NotImplementedException(); + + /// + public override Task ClearPermissionOverridesAsync() => throw new System.NotImplementedException(); + + /// + public override Task PagesAsync() => throw new System.NotImplementedException(); + + /// + public override async Task NewPageAsync() + { + var context = await UserContext.CreateBrowserContextAsync(WebDriverBiDi.BrowsingContext.CreateType.Tab).ConfigureAwait(false); + + if (!_pages.TryGetValue(context, out var page)) + { + throw new PuppeteerException("Page is not found"); + } + + if (DefaultViewport != null) + { + try + { + await page.SetViewportAsync(DefaultViewport).ConfigureAwait(false); + } + catch + { + // No support for setViewport in Firefox. + } + } + + return page; + } + + /// + public override Task CloseAsync() => throw new System.NotImplementedException(); + + /// + public override ITarget[] Targets() + => _targets.SelectMany(target => + (ITarget[])[ + target.Value.BidiPageTarget, + .. target.Value.FrameTargets.Values.ToArray(), + .. target.Value.WorkerTargets.Values.ToArray(), + ]).ToArray(); + + internal static BidiBrowserContext From( + BidiBrowser browser, + UserContext userContext, + BidiBrowserContextOptions options) + { + var context = new BidiBrowserContext(browser, userContext, options); + context.Initialize(); + return context; + } + + private void Initialize() + { + // Create targets for existing browsing contexts. + foreach (var browsingContext in UserContext.BrowsingContexts) + { + CreatePage(browsingContext); + } + + UserContext.BrowsingContextCreated += (sender, args) => + { + var browsingContext = args.BrowsingContext; + var page = CreatePage(browsingContext); + + // We need to wait for the DOMContentLoaded as the + // browsingContext still may be navigating from the about:blank + browsingContext.DomContentLoaded += (o, eventArgs) => + { + if (browsingContext.OriginalOpener == null) + { + return; + } + + foreach (var context in UserContext.BrowsingContexts) + { + if (context.Id != browsingContext.OriginalOpener) + { + continue; + } + + if (_pages.TryGetValue(context, out var originalOpenerPage)) + { + originalOpenerPage.OnPopup(page); + } + } + }; + }; + } + + private BidiPage CreatePage(BrowsingContext browsingContext) + { + var page = BidiPage.From(this, browsingContext); + _pages.AddOrUpdate(browsingContext, page, (_, _) => page); + + var pageTarget = new BidiPageTarget(page); + var targetInfo = new BidiPageTargetInfo { BidiPageTarget = pageTarget }; + _targets.TryAdd(page, targetInfo); + + page.FrameAttached += (_, e) => + { + var bidiFrame = (BidiFrame)e.Frame; + var frameTarget = new BidiFrameTarget(bidiFrame); + targetInfo.FrameTargets.TryAdd(bidiFrame, frameTarget); + }; + + page.FrameNavigated += (_, e) => + { + var bidiFrame = (BidiFrame)e.Frame; + OnTargetChanged(new TargetChangedArgs( + targetInfo.FrameTargets.TryGetValue(bidiFrame, out var frameTarget) + ? frameTarget + : pageTarget)); + }; + + page.FrameDetached += (_, e) => + { + var bidiFrame = (BidiFrame)e.Frame; + if (targetInfo.FrameTargets.TryRemove(bidiFrame, out var frameTarget)) + { + OnTargetDestroyed(new TargetChangedArgs(frameTarget)); + } + }; + + page.WorkerCreated += (_, e) => + { + var bidiWorker = (BidiWebWorker)e.Worker; + var workerTarget = new BidiWorkerTarget(bidiWorker); + targetInfo.WorkerTargets.TryAdd(bidiWorker, workerTarget); + }; + + page.WorkerDestroyed += (_, e) => + { + var bidiWorker = (BidiWebWorker)e.Worker; + if (targetInfo.WorkerTargets.TryRemove(bidiWorker, out var workerTarget)) + { + OnTargetDestroyed(new TargetChangedArgs(workerTarget)); + } + }; + + page.Close += (_, _) => + { + if (_targets.TryRemove(page, out _)) + { + OnTargetDestroyed(new TargetChangedArgs(pageTarget)); + } + }; + + OnTargetCreated(new TargetChangedArgs(pageTarget)); + return page; + } + + private class BidiPageTargetInfo + { + public BidiPageTarget BidiPageTarget { get; set; } + + public ConcurrentDictionary FrameTargets { get; set; } = new(); + + public ConcurrentDictionary WorkerTargets { get; set; } = new(); + } +} + + diff --git a/lib/PuppeteerSharp/Bidi/BidiBrowserContextOptions.cs b/lib/PuppeteerSharp/Bidi/BidiBrowserContextOptions.cs new file mode 100644 index 000000000..59900a772 --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiBrowserContextOptions.cs @@ -0,0 +1,30 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using WebDriverBiDi.BrowsingContext; + +namespace PuppeteerSharp.Bidi; + +internal class BidiBrowserContextOptions +{ + public ViewPortOptions DefaultViewport { get; set; } +} diff --git a/lib/PuppeteerSharp/Bidi/BidiBrowserTarget.cs b/lib/PuppeteerSharp/Bidi/BidiBrowserTarget.cs new file mode 100644 index 000000000..b05fc82b2 --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiBrowserTarget.cs @@ -0,0 +1,42 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System.Threading.Tasks; + +namespace PuppeteerSharp.Bidi; + +internal class BidiBrowserTarget(BidiBrowser bidiBrowser) : Target +{ + public override string Url { get; } + + public override TargetType Type { get; } + + public override ITarget Opener { get; } + + internal override BrowserContext BrowserContext { get; } + + internal override Browser Browser => bidiBrowser; + + public override Task AsPageAsync() => throw new System.NotImplementedException(); + + public override Task CreateCDPSessionAsync() => throw new System.NotImplementedException(); +} diff --git a/lib/PuppeteerSharp/Bidi/BidiCdpSession.cs b/lib/PuppeteerSharp/Bidi/BidiCdpSession.cs new file mode 100644 index 000000000..61b3e9a1e --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiCdpSession.cs @@ -0,0 +1,85 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace PuppeteerSharp.Bidi; + +/// +public class BidiCdpSession : ICDPSession +{ + // I don't like the idea of having this static field. But this is the original implementation. + private static readonly ConcurrentDictionary _sessions = new(); + + /// + public event EventHandler MessageReceived; + + /// + public event EventHandler Disconnected; + + /// + public event EventHandler SessionAttached; + + /// + public event EventHandler SessionDetached; + + /// + public ILoggerFactory LoggerFactory { get; } + + /// + public string Id { get; } + + /// + public string CloseReason { get; } + + internal static IEnumerable Sessions => _sessions.Values; + + internal BidiFrame Frame { get; set; } + + /// + public Task SendAsync(string method, object args = null, CommandOptions options = null) => throw new NotImplementedException(); + + /// + public Task SendAsync(string method, object args = null, bool waitForCallback = true, CommandOptions options = null) => throw new NotImplementedException(); + + /// + public Task DetachAsync() => throw new NotImplementedException(); + + internal void OnClose() + { + throw new NotImplementedException(); + } + + internal virtual void OnSessionAttached(SessionEventArgs e) => SessionAttached?.Invoke(this, e); + + internal virtual void OnSessionDetached(SessionEventArgs e) => SessionDetached?.Invoke(this, e); + + internal virtual void OnDisconnected() => Disconnected?.Invoke(this, EventArgs.Empty); + + internal virtual void OnMessageReceived(MessageEventArgs e) => MessageReceived?.Invoke(this, e); +} diff --git a/lib/PuppeteerSharp/Bidi/BidiFrame.cs b/lib/PuppeteerSharp/Bidi/BidiFrame.cs new file mode 100644 index 000000000..10df07299 --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiFrame.cs @@ -0,0 +1,229 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using PuppeteerSharp.Bidi.Core; + +namespace PuppeteerSharp.Bidi; + +/// +public class BidiFrame : Frame +{ + private readonly ConcurrentDictionary _frames = new(); + + internal BidiFrame(BidiPage parentPage, BidiFrame parentFrame, BrowsingContext browsingContext) + { + ParentPage = parentPage; + ParentFrame = parentFrame; + BrowsingContext = browsingContext; + } + + /// + public override IReadOnlyCollection