Skip to content

Commit bed10ff

Browse files
authored
feat(testing-harness): allow connecting remotely (#3098)
1 parent 732ba38 commit bed10ff

File tree

12 files changed

+278
-72
lines changed

12 files changed

+278
-72
lines changed

src/Playwright.MSTest/BrowserService.cs

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using System;
2626
using System.Collections.Generic;
2727
using System.Globalization;
28+
using System.Linq;
2829
using System.Text.Json;
2930
using System.Text.Json.Serialization;
3031
using System.Threading.Tasks;
@@ -34,50 +35,66 @@ namespace Microsoft.Playwright.MSTest;
3435

3536
internal class BrowserService : IWorkerService
3637
{
37-
public IBrowser Browser { get; internal set; } = null!;
38-
39-
public Task ResetAsync() => Task.CompletedTask;
40-
41-
public Task DisposeAsync() => Browser?.CloseAsync() ?? Task.CompletedTask;
38+
public IBrowser Browser { get; private set; }
4239

4340
private BrowserService(IBrowser browser)
4441
{
4542
Browser = browser;
4643
}
4744

48-
public static Task<BrowserService> Register(WorkerAwareTest test, IBrowserType browserType)
45+
public static Task<BrowserService> Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions)
4946
{
50-
return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType).ConfigureAwait(false)));
47+
return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false)));
5148
}
5249

53-
private static async Task<IBrowser> CreateBrowser(IBrowserType browserType)
50+
private static async Task<IBrowser> CreateBrowser(IBrowserType browserType, (string WSEndpoint, BrowserTypeConnectOptions? Options)? connectOptions)
51+
{
52+
if (connectOptions.HasValue && connectOptions.Value.WSEndpoint != null)
53+
{
54+
var options = new BrowserTypeConnectOptions(connectOptions?.Options ?? new());
55+
var headers = options.Headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? [];
56+
headers.Add("x-playwright-launch-options", JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
57+
options.Headers = headers;
58+
return await browserType.ConnectAsync(connectOptions!.Value.WSEndpoint, options).ConfigureAwait(false);
59+
}
60+
61+
var legacyBrowser = await ConnectBasedOnEnv(browserType);
62+
if (legacyBrowser != null)
63+
{
64+
return legacyBrowser;
65+
}
66+
return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false);
67+
}
68+
69+
// TODO: Remove at some point
70+
private static async Task<IBrowser?> ConnectBasedOnEnv(IBrowserType browserType)
5471
{
5572
var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN");
5673
var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL");
5774

5875
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl))
5976
{
60-
return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false);
77+
return null;
6178
}
62-
else
79+
80+
var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? "<loopback>";
81+
var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux");
82+
var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
83+
var apiVersion = "2023-10-01-preview";
84+
var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}";
85+
86+
return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions
6387
{
64-
var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? "<loopback>";
65-
var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux");
66-
var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
67-
var apiVersion = "2023-10-01-preview";
68-
var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}";
69-
var connectOptions = new BrowserTypeConnectOptions
88+
Timeout = 3 * 60 * 1000,
89+
ExposeNetwork = exposeNetwork,
90+
Headers = new Dictionary<string, string>
7091
{
71-
Timeout = 3 * 60 * 1000,
72-
ExposeNetwork = exposeNetwork,
73-
Headers = new Dictionary<string, string>
74-
{
75-
["Authorization"] = $"Bearer {accessToken}",
76-
["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })
77-
}
78-
};
79-
80-
return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false);
81-
}
92+
["Authorization"] = $"Bearer {accessToken}",
93+
["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })
94+
}
95+
}).ConfigureAwait(false);
8296
}
97+
98+
public Task ResetAsync() => Task.CompletedTask;
99+
public Task DisposeAsync() => Browser.CloseAsync();
83100
}

src/Playwright.MSTest/BrowserTest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public async Task<IBrowserContext> NewContextAsync(BrowserNewContextOptions? opt
4444
[TestInitialize]
4545
public async Task BrowserSetup()
4646
{
47-
var service = await BrowserService.Register(this, BrowserType).ConfigureAwait(false);
47+
var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false);
4848
Browser = service.Browser;
4949
}
5050

@@ -61,4 +61,6 @@ public async Task BrowserTearDown()
6161
_contexts.Clear();
6262
Browser = null!;
6363
}
64+
65+
public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null);
6466
}

src/Playwright.NUnit/BrowserService.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using System;
2626
using System.Collections.Generic;
2727
using System.Globalization;
28+
using System.Linq;
2829
using System.Text.Json;
2930
using System.Text.Json.Serialization;
3031
using System.Threading.Tasks;
@@ -41,27 +42,48 @@ private BrowserService(IBrowser browser)
4142
Browser = browser;
4243
}
4344

44-
public static Task<BrowserService> Register(WorkerAwareTest test, IBrowserType browserType)
45+
public static Task<BrowserService> Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions)
4546
{
46-
return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType).ConfigureAwait(false)));
47+
return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false)));
4748
}
4849

49-
private static async Task<IBrowser> CreateBrowser(IBrowserType browserType)
50+
private static async Task<IBrowser> CreateBrowser(IBrowserType browserType, (string WSEndpoint, BrowserTypeConnectOptions? Options)? connectOptions)
51+
{
52+
if (connectOptions.HasValue && connectOptions.Value.WSEndpoint != null)
53+
{
54+
var options = new BrowserTypeConnectOptions(connectOptions?.Options ?? new());
55+
var headers = options.Headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? [];
56+
headers.Add("x-playwright-launch-options", JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
57+
options.Headers = headers;
58+
return await browserType.ConnectAsync(connectOptions!.Value.WSEndpoint, options).ConfigureAwait(false);
59+
}
60+
61+
var legacyBrowser = await ConnectBasedOnEnv(browserType);
62+
if (legacyBrowser != null)
63+
{
64+
return legacyBrowser;
65+
}
66+
return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false);
67+
}
68+
69+
// TODO: Remove at some point
70+
private static async Task<IBrowser?> ConnectBasedOnEnv(IBrowserType browserType)
5071
{
5172
var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN");
5273
var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL");
5374

5475
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl))
5576
{
56-
return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false);
77+
return null;
5778
}
5879

5980
var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? "<loopback>";
6081
var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux");
6182
var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
6283
var apiVersion = "2023-10-01-preview";
6384
var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}";
64-
var connectOptions = new BrowserTypeConnectOptions
85+
86+
return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions
6587
{
6688
Timeout = 3 * 60 * 1000,
6789
ExposeNetwork = exposeNetwork,
@@ -70,9 +92,7 @@ private static async Task<IBrowser> CreateBrowser(IBrowserType browserType)
7092
["Authorization"] = $"Bearer {accessToken}",
7193
["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })
7294
}
73-
};
74-
75-
return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false);
95+
}).ConfigureAwait(false);
7696
}
7797

7898
public Task ResetAsync() => Task.CompletedTask;

src/Playwright.NUnit/BrowserTest.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task<IBrowserContext> NewContext(BrowserNewContextOptions? options
4343
[SetUp]
4444
public async Task BrowserSetup()
4545
{
46-
var service = await BrowserService.Register(this, BrowserType).ConfigureAwait(false);
46+
var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false);
4747
Browser = service.Browser;
4848
}
4949

@@ -60,4 +60,6 @@ public async Task BrowserTearDown()
6060
_contexts.Clear();
6161
Browser = null!;
6262
}
63+
64+
public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null);
6365
}

src/Playwright.TestingHarnessTest/package-lock.json

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Playwright.TestingHarnessTest/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"name": "playwright.testingharnesstest",
33
"private": true,
44
"devDependencies": {
5-
"@playwright/test": "^1.48.2",
65
"@types/node": "^22.12.0",
7-
"fast-xml-parser": "^4.5.0"
6+
"fast-xml-parser": "^4.5.0",
7+
"@playwright/test": "1.50.0"
88
}
99
}

src/Playwright.TestingHarnessTest/tests/baseTest.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs';
22
import http from 'http';
33
import path from 'path';
44
import childProcess from 'child_process';
5-
import { test as base } from '@playwright/test';
5+
import { test as base, BrowserServer } from '@playwright/test';
66
import { XMLParser } from 'fast-xml-parser';
77
import { AddressInfo } from 'net';
88

@@ -21,6 +21,7 @@ export const test = base.extend<{
2121
proxyServer: ProxyServer;
2222
testMode: 'nunit' | 'mstest' | 'xunit';
2323
runTest: (files: Record<string, string>, command: string, env?: NodeJS.ProcessEnv) => Promise<RunResult>;
24+
launchServer: ({ port: number }) => Promise<void>;
2425
}>({
2526
proxyServer: async ({}, use) => {
2627
const proxyServer = new ProxyServer();
@@ -29,6 +30,14 @@ export const test = base.extend<{
2930
await proxyServer.stop();
3031
},
3132
testMode: null,
33+
launchServer: async ({ playwright }, use) => {
34+
const servers: BrowserServer[] = [];
35+
await use(async ({port}: {port: number}) => {
36+
servers.push(await playwright.chromium.launchServer({ port }));
37+
});
38+
for (const server of servers)
39+
await server.close();
40+
},
3241
runTest: async ({ testMode }, use, testInfo) => {
3342
const testResults: RunResult[] = [];
3443
await use(async (files, command, env) => {

src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,48 @@ test.describe('Expect() timeout', () => {
491491
expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 123ms")
492492
});
493493
});
494+
495+
test.describe('ConnectOptions', () => {
496+
const ExampleTestWithConnectOptions = `
497+
using System;
498+
using System.Threading.Tasks;
499+
using Microsoft.Playwright;
500+
using Microsoft.Playwright.MSTest;
501+
using Microsoft.VisualStudio.TestTools.UnitTesting;
502+
503+
namespace Playwright.TestingHarnessTest.MSTest;
504+
505+
[TestClass]
506+
public class <class-name> : PageTest
507+
{
508+
[TestMethod]
509+
public async Task Test()
510+
{
511+
await Page.GotoAsync("about:blank");
512+
}
513+
public override async Task<(string, BrowserTypeConnectOptions)?> ConnectOptionsAsync()
514+
{
515+
return ("http://127.0.0.1:1234", null);
516+
}
517+
}`;
518+
519+
test('should fail when the server is not reachable', async ({ runTest }) => {
520+
const result = await runTest({
521+
'ExampleTests.cs': ExampleTestWithConnectOptions,
522+
}, 'dotnet test');
523+
expect(result.passed).toBe(0);
524+
expect(result.failed).toBe(1);
525+
expect(result.total).toBe(1);
526+
expect(result.rawStdout).toContain('connect ECONNREFUSED 127.0.0.1:1234')
527+
});
528+
529+
test('should pass when the server is reachable', async ({ runTest, launchServer }) => {
530+
await launchServer({ port: 1234 });
531+
const result = await runTest({
532+
'ExampleTests.cs': ExampleTestWithConnectOptions,
533+
}, 'dotnet test');
534+
expect(result.passed).toBe(1);
535+
expect(result.failed).toBe(0);
536+
expect(result.total).toBe(1);
537+
});
538+
});

0 commit comments

Comments
 (0)