diff --git a/src/Playwright.MSTest/BrowserService.cs b/src/Playwright.MSTest/BrowserService.cs index 3c6269163..84c06274b 100644 --- a/src/Playwright.MSTest/BrowserService.cs +++ b/src/Playwright.MSTest/BrowserService.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -34,50 +35,66 @@ namespace Microsoft.Playwright.MSTest; internal class BrowserService : IWorkerService { - public IBrowser Browser { get; internal set; } = null!; - - public Task ResetAsync() => Task.CompletedTask; - - public Task DisposeAsync() => Browser?.CloseAsync() ?? Task.CompletedTask; + public IBrowser Browser { get; private set; } private BrowserService(IBrowser browser) { Browser = browser; } - public static Task Register(WorkerAwareTest test, IBrowserType browserType) + public static Task Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions) { - return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType).ConfigureAwait(false))); + return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false))); } - private static async Task CreateBrowser(IBrowserType browserType) + private static async Task CreateBrowser(IBrowserType browserType, (string WSEndpoint, BrowserTypeConnectOptions? Options)? connectOptions) + { + if (connectOptions.HasValue && connectOptions.Value.WSEndpoint != null) + { + var options = new BrowserTypeConnectOptions(connectOptions?.Options ?? new()); + var headers = options.Headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? []; + headers.Add("x-playwright-launch-options", JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); + options.Headers = headers; + return await browserType.ConnectAsync(connectOptions!.Value.WSEndpoint, options).ConfigureAwait(false); + } + + var legacyBrowser = await ConnectBasedOnEnv(browserType); + if (legacyBrowser != null) + { + return legacyBrowser; + } + return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + } + + // TODO: Remove at some point + private static async Task ConnectBasedOnEnv(IBrowserType browserType) { var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN"); var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL"); if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl)) { - return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + return null; } - else + + var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? ""; + var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux"); + var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture)); + var apiVersion = "2023-10-01-preview"; + var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}"; + + return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions { - var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? ""; - var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux"); - var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture)); - var apiVersion = "2023-10-01-preview"; - var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}"; - var connectOptions = new BrowserTypeConnectOptions + Timeout = 3 * 60 * 1000, + ExposeNetwork = exposeNetwork, + Headers = new Dictionary { - Timeout = 3 * 60 * 1000, - ExposeNetwork = exposeNetwork, - Headers = new Dictionary - { - ["Authorization"] = $"Bearer {accessToken}", - ["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }) - } - }; - - return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false); - } + ["Authorization"] = $"Bearer {accessToken}", + ["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }) + } + }).ConfigureAwait(false); } + + public Task ResetAsync() => Task.CompletedTask; + public Task DisposeAsync() => Browser.CloseAsync(); } diff --git a/src/Playwright.MSTest/BrowserTest.cs b/src/Playwright.MSTest/BrowserTest.cs index f34bec19f..904ae25bd 100644 --- a/src/Playwright.MSTest/BrowserTest.cs +++ b/src/Playwright.MSTest/BrowserTest.cs @@ -44,7 +44,7 @@ public async Task NewContextAsync(BrowserNewContextOptions? opt [TestInitialize] public async Task BrowserSetup() { - var service = await BrowserService.Register(this, BrowserType).ConfigureAwait(false); + var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false); Browser = service.Browser; } @@ -61,4 +61,6 @@ public async Task BrowserTearDown() _contexts.Clear(); Browser = null!; } + + public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null); } diff --git a/src/Playwright.NUnit/BrowserService.cs b/src/Playwright.NUnit/BrowserService.cs index 9f0857ec0..0457008df 100644 --- a/src/Playwright.NUnit/BrowserService.cs +++ b/src/Playwright.NUnit/BrowserService.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -41,19 +42,39 @@ private BrowserService(IBrowser browser) Browser = browser; } - public static Task Register(WorkerAwareTest test, IBrowserType browserType) + public static Task Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions) { - return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType).ConfigureAwait(false))); + return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false))); } - private static async Task CreateBrowser(IBrowserType browserType) + private static async Task CreateBrowser(IBrowserType browserType, (string WSEndpoint, BrowserTypeConnectOptions? Options)? connectOptions) + { + if (connectOptions.HasValue && connectOptions.Value.WSEndpoint != null) + { + var options = new BrowserTypeConnectOptions(connectOptions?.Options ?? new()); + var headers = options.Headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? []; + headers.Add("x-playwright-launch-options", JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); + options.Headers = headers; + return await browserType.ConnectAsync(connectOptions!.Value.WSEndpoint, options).ConfigureAwait(false); + } + + var legacyBrowser = await ConnectBasedOnEnv(browserType); + if (legacyBrowser != null) + { + return legacyBrowser; + } + return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + } + + // TODO: Remove at some point + private static async Task ConnectBasedOnEnv(IBrowserType browserType) { var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN"); var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL"); if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl)) { - return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + return null; } var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? ""; @@ -61,7 +82,8 @@ private static async Task CreateBrowser(IBrowserType browserType) var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture)); var apiVersion = "2023-10-01-preview"; var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}"; - var connectOptions = new BrowserTypeConnectOptions + + return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions { Timeout = 3 * 60 * 1000, ExposeNetwork = exposeNetwork, @@ -70,9 +92,7 @@ private static async Task CreateBrowser(IBrowserType browserType) ["Authorization"] = $"Bearer {accessToken}", ["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }) } - }; - - return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false); + }).ConfigureAwait(false); } public Task ResetAsync() => Task.CompletedTask; diff --git a/src/Playwright.NUnit/BrowserTest.cs b/src/Playwright.NUnit/BrowserTest.cs index 3f4592aa2..529fe64e5 100644 --- a/src/Playwright.NUnit/BrowserTest.cs +++ b/src/Playwright.NUnit/BrowserTest.cs @@ -43,7 +43,7 @@ public async Task NewContext(BrowserNewContextOptions? options [SetUp] public async Task BrowserSetup() { - var service = await BrowserService.Register(this, BrowserType).ConfigureAwait(false); + var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false); Browser = service.Browser; } @@ -60,4 +60,6 @@ public async Task BrowserTearDown() _contexts.Clear(); Browser = null!; } + + public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null); } diff --git a/src/Playwright.TestingHarnessTest/package-lock.json b/src/Playwright.TestingHarnessTest/package-lock.json index a984611bf..ccff052ea 100644 --- a/src/Playwright.TestingHarnessTest/package-lock.json +++ b/src/Playwright.TestingHarnessTest/package-lock.json @@ -7,19 +7,19 @@ "": { "name": "playwright.testingharnesstest", "devDependencies": { - "@playwright/test": "^1.48.2", + "@playwright/test": "1.50.0", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } }, "node_modules/@playwright/test": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", - "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.48.2" + "playwright": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -77,13 +77,13 @@ } }, "node_modules/playwright": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", - "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.48.2" + "playwright-core": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -96,9 +96,9 @@ } }, "node_modules/playwright-core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", - "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -124,12 +124,12 @@ }, "dependencies": { "@playwright/test": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", - "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", "dev": true, "requires": { - "playwright": "1.48.2" + "playwright": "1.50.0" } }, "@types/node": { @@ -158,19 +158,19 @@ "optional": true }, "playwright": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", - "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.48.2" + "playwright-core": "1.50.0" } }, "playwright-core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", - "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", "dev": true }, "strnum": { diff --git a/src/Playwright.TestingHarnessTest/package.json b/src/Playwright.TestingHarnessTest/package.json index e396e232c..2f1df7526 100644 --- a/src/Playwright.TestingHarnessTest/package.json +++ b/src/Playwright.TestingHarnessTest/package.json @@ -2,8 +2,8 @@ "name": "playwright.testingharnesstest", "private": true, "devDependencies": { - "@playwright/test": "^1.48.2", "@types/node": "^22.12.0", - "fast-xml-parser": "^4.5.0" + "fast-xml-parser": "^4.5.0", + "@playwright/test": "1.50.0" } } diff --git a/src/Playwright.TestingHarnessTest/tests/baseTest.ts b/src/Playwright.TestingHarnessTest/tests/baseTest.ts index 6561aa0cb..024c38d77 100644 --- a/src/Playwright.TestingHarnessTest/tests/baseTest.ts +++ b/src/Playwright.TestingHarnessTest/tests/baseTest.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import http from 'http'; import path from 'path'; import childProcess from 'child_process'; -import { test as base } from '@playwright/test'; +import { test as base, BrowserServer } from '@playwright/test'; import { XMLParser } from 'fast-xml-parser'; import { AddressInfo } from 'net'; @@ -21,6 +21,7 @@ export const test = base.extend<{ proxyServer: ProxyServer; testMode: 'nunit' | 'mstest' | 'xunit'; runTest: (files: Record, command: string, env?: NodeJS.ProcessEnv) => Promise; + launchServer: ({ port: number }) => Promise; }>({ proxyServer: async ({}, use) => { const proxyServer = new ProxyServer(); @@ -29,6 +30,14 @@ export const test = base.extend<{ await proxyServer.stop(); }, testMode: null, + launchServer: async ({ playwright }, use) => { + const servers: BrowserServer[] = []; + await use(async ({port}: {port: number}) => { + servers.push(await playwright.chromium.launchServer({ port })); + }); + for (const server of servers) + await server.close(); + }, runTest: async ({ testMode }, use, testInfo) => { const testResults: RunResult[] = []; await use(async (files, command, env) => { diff --git a/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts index 16b6b70d2..80ef324d1 100644 --- a/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts +++ b/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts @@ -491,3 +491,48 @@ test.describe('Expect() timeout', () => { expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 123ms") }); }); + +test.describe('ConnectOptions', () => { + const ExampleTestWithConnectOptions = ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + } + public override async Task<(string, BrowserTypeConnectOptions)?> ConnectOptionsAsync() + { + return ("http://127.0.0.1:1234", null); + } + }`; + + test('should fail when the server is not reachable', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain('connect ECONNREFUSED 127.0.0.1:1234') + }); + + test('should pass when the server is reachable', async ({ runTest, launchServer }) => { + await launchServer({ port: 1234 }); + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + }); +}); diff --git a/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts index 4e64c3f15..495e06432 100644 --- a/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts +++ b/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts @@ -487,3 +487,48 @@ test.describe('Expect() timeout', () => { expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 123ms") }); }); + +test.describe('ConnectOptions', () => { + const ExampleTestWithConnectOptions = ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.NUnit; + using NUnit.Framework; + + namespace Playwright.TestingHarnessTest.NUnit; + + public class : PageTest + { + [Test] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + } + + public override async Task<(string, BrowserTypeConnectOptions)?> ConnectOptionsAsync() + { + return ("http://127.0.0.1:1234", null); + } + }`; + + test('should fail when the server is not reachable', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain('connect ECONNREFUSED 127.0.0.1:1234') + }); + + test('should pass when the server is reachable', async ({ runTest, launchServer }) => { + await launchServer({ port: 1234 }); + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + }); +}); diff --git a/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts index e8db07be8..d5c2896f9 100644 --- a/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts +++ b/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts @@ -537,3 +537,47 @@ test.describe('Expect() timeout', () => { expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 123ms") }); }); + +test.describe('ConnectOptions', () => { + const ExampleTestWithConnectOptions = ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + } + public override async Task<(string, BrowserTypeConnectOptions)?> ConnectOptionsAsync() + { + return ("http://127.0.0.1:1234", null); + } + }`; + + test('should fail when the server is not reachable', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain('connect ECONNREFUSED 127.0.0.1:1234') + }); + + test('should pass when the server is reachable', async ({ runTest, launchServer }) => { + await launchServer({ port: 1234 }); + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + }); +}); diff --git a/src/Playwright.Xunit/BrowserService.cs b/src/Playwright.Xunit/BrowserService.cs index 316abc7f9..0b5d0f898 100644 --- a/src/Playwright.Xunit/BrowserService.cs +++ b/src/Playwright.Xunit/BrowserService.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -41,19 +42,39 @@ private BrowserService(IBrowser browser) Browser = browser; } - public static Task Register(WorkerAwareTest test, IBrowserType browserType) + public static Task Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions) { - return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType).ConfigureAwait(false))); + return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false))); } - private static async Task CreateBrowser(IBrowserType browserType) + private static async Task CreateBrowser(IBrowserType browserType, (string WSEndpoint, BrowserTypeConnectOptions? Options)? connectOptions) + { + if (connectOptions.HasValue && connectOptions.Value.WSEndpoint != null) + { + var options = new BrowserTypeConnectOptions(connectOptions?.Options ?? new()); + var headers = options.Headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? []; + headers.Add("x-playwright-launch-options", JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); + options.Headers = headers; + return await browserType.ConnectAsync(connectOptions!.Value.WSEndpoint, options).ConfigureAwait(false); + } + + var legacyBrowser = await ConnectBasedOnEnv(browserType); + if (legacyBrowser != null) + { + return legacyBrowser; + } + return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + } + + // TODO: Remove at some point + private static async Task ConnectBasedOnEnv(IBrowserType browserType) { var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN"); var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL"); if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl)) { - return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + return null; } var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? ""; @@ -61,7 +82,8 @@ private static async Task CreateBrowser(IBrowserType browserType) var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture)); var apiVersion = "2023-10-01-preview"; var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}"; - var connectOptions = new BrowserTypeConnectOptions + + return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions { Timeout = 3 * 60 * 1000, ExposeNetwork = exposeNetwork, @@ -70,9 +92,7 @@ private static async Task CreateBrowser(IBrowserType browserType) ["Authorization"] = $"Bearer {accessToken}", ["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }) } - }; - - return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false); + }).ConfigureAwait(false); } public Task ResetAsync() => Task.CompletedTask; diff --git a/src/Playwright.Xunit/BrowserTest.cs b/src/Playwright.Xunit/BrowserTest.cs index 0540bfb17..e76e791a7 100644 --- a/src/Playwright.Xunit/BrowserTest.cs +++ b/src/Playwright.Xunit/BrowserTest.cs @@ -42,7 +42,7 @@ public async Task NewContext(BrowserNewContextOptions? options public override async Task InitializeAsync() { await base.InitializeAsync().ConfigureAwait(false); - var service = await BrowserService.Register(this, BrowserType).ConfigureAwait(false); + var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false); Browser = service.Browser; } @@ -59,4 +59,6 @@ public override async Task DisposeAsync() Browser = null!; await base.DisposeAsync().ConfigureAwait(false); } + + public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null); }