Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(testing-harness): allow connecting remotely #3098

Merged
merged 5 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 44 additions & 27 deletions src/Playwright.MSTest/BrowserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<BrowserService> Register(WorkerAwareTest test, IBrowserType browserType)
public static Task<BrowserService> 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<IBrowser> CreateBrowser(IBrowserType browserType)
private static async Task<IBrowser> 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<IBrowser?> 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") ?? "<loopback>";
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") ?? "<loopback>";
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<string, string>
{
Timeout = 3 * 60 * 1000,
ExposeNetwork = exposeNetwork,
Headers = new Dictionary<string, string>
{
["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();
}
4 changes: 3 additions & 1 deletion src/Playwright.MSTest/BrowserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task<IBrowserContext> 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;
}

Expand All @@ -61,4 +61,6 @@ public async Task BrowserTearDown()
_contexts.Clear();
Browser = null!;
}

public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null);
}
36 changes: 28 additions & 8 deletions src/Playwright.NUnit/BrowserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,27 +42,48 @@ private BrowserService(IBrowser browser)
Browser = browser;
}

public static Task<BrowserService> Register(WorkerAwareTest test, IBrowserType browserType)
public static Task<BrowserService> 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<IBrowser> CreateBrowser(IBrowserType browserType)
private static async Task<IBrowser> 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<IBrowser?> 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") ?? "<loopback>";
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

return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions
{
Timeout = 3 * 60 * 1000,
ExposeNetwork = exposeNetwork,
Expand All @@ -70,9 +92,7 @@ private static async Task<IBrowser> 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;
Expand Down
4 changes: 3 additions & 1 deletion src/Playwright.NUnit/BrowserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task<IBrowserContext> 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;
}

Expand All @@ -60,4 +60,6 @@ public async Task BrowserTearDown()
_contexts.Clear();
Browser = null!;
}

public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null);
}
46 changes: 23 additions & 23 deletions src/Playwright.TestingHarnessTest/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Playwright.TestingHarnessTest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
11 changes: 10 additions & 1 deletion src/Playwright.TestingHarnessTest/tests/baseTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -21,6 +21,7 @@ export const test = base.extend<{
proxyServer: ProxyServer;
testMode: 'nunit' | 'mstest' | 'xunit';
runTest: (files: Record<string, string>, command: string, env?: NodeJS.ProcessEnv) => Promise<RunResult>;
launchServer: ({ port: number }) => Promise<void>;
}>({
proxyServer: async ({}, use) => {
const proxyServer = new ProxyServer();
Expand All @@ -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) => {
Expand Down
45 changes: 45 additions & 0 deletions src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <class-name> : 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);
});
});
Loading
Loading