diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 50cc7ac92..78b50427c 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -76,7 +76,8 @@ jobs:
{ name: "Testcontainers.RavenDb", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" },
- { name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" }
+ { name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" },
+ { name: "Testcontainers.Playwright", runs-on: "ubuntu-22.04" }
]
runs-on: ${{ matrix.test-projects.runs-on }}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2d3b02615..521b65572 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -63,5 +63,6 @@
+
-
\ No newline at end of file
+
diff --git a/Testcontainers.sln b/Testcontainers.sln
index 10cae3fc2..e607c6de2 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -195,6 +195,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Playwright", "src\Testcontainers.Playwright\Testcontainers.Playwright.csproj", "{275ADBA5-498E-45FA-AD3F-0E5EE7996D99}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Playwright.Tests", "tests\Testcontainers.Playwright.Tests\Testcontainers.Playwright.Tests.csproj", "{A56D182C-8347-4CBE-9FC8-F2C98648F134}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -568,6 +572,14 @@ Global
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {275ADBA5-498E-45FA-AD3F-0E5EE7996D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {275ADBA5-498E-45FA-AD3F-0E5EE7996D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {275ADBA5-498E-45FA-AD3F-0E5EE7996D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {275ADBA5-498E-45FA-AD3F-0E5EE7996D99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A56D182C-8347-4CBE-9FC8-F2C98648F134}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A56D182C-8347-4CBE-9FC8-F2C98648F134}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A56D182C-8347-4CBE-9FC8-F2C98648F134}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A56D182C-8347-4CBE-9FC8-F2C98648F134}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -661,5 +673,7 @@ Global
{9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {275ADBA5-498E-45FA-AD3F-0E5EE7996D99} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {A56D182C-8347-4CBE-9FC8-F2C98648F134} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
diff --git a/src/Testcontainers.Playwright/PlaywrightBrowser.cs b/src/Testcontainers.Playwright/PlaywrightBrowser.cs
new file mode 100644
index 000000000..96ab688b4
--- /dev/null
+++ b/src/Testcontainers.Playwright/PlaywrightBrowser.cs
@@ -0,0 +1,52 @@
+namespace Testcontainers.Playwright;
+
+///
+/// Playwright browser configuration.
+///
+[PublicAPI]
+public readonly struct PlaywrightBrowser
+{
+ ///
+ /// Gets the Playwright standalone Chrome configuration.
+ ///
+ public static readonly PlaywrightBrowser Chrome = new PlaywrightBrowser("jacoblincool/playwright:chrome-server");
+
+ ///
+ /// Gets the Playwright standalone Chromium configuration.
+ ///
+ public static readonly PlaywrightBrowser Chromium = new PlaywrightBrowser("jacoblincool/playwright:chromium-server");
+
+ ///
+ /// Gets the Playwright standalone Firefox configuration.
+ ///
+ public static readonly PlaywrightBrowser Firefox = new PlaywrightBrowser("jacoblincool/playwright:firefox-server");
+
+ ///
+ /// Gets the Playwright standalone Edge configuration.
+ ///
+ public static readonly PlaywrightBrowser Edge = new PlaywrightBrowser("jacoblincool/playwright:msedge-server");
+
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The Playwright standalone Docker image.
+ public PlaywrightBrowser(string image)
+ : this(new DockerImage(image))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The Playwright standalone Docker image.
+ public PlaywrightBrowser(IImage image)
+ {
+ Image = image;
+ }
+ ///
+ /// Gets the Playwright standalone Docker image.
+ ///
+ [NotNull]
+ public IImage Image { get; }
+}
diff --git a/src/Testcontainers.Playwright/PlaywrightBuilder.cs b/src/Testcontainers.Playwright/PlaywrightBuilder.cs
new file mode 100644
index 000000000..bb181cde0
--- /dev/null
+++ b/src/Testcontainers.Playwright/PlaywrightBuilder.cs
@@ -0,0 +1,98 @@
+namespace Testcontainers.Playwright;
+
+///
+///
+/// Find further information about the Playwright image, here: https://playwright.dev/dotnet/docs/docker.
+///
+[PublicAPI]
+public class PlaywrightBuilder : ContainerBuilder
+{
+ private const ushort PlaywrightPort = 53333;
+ private const string PlaywrightEndpointPath = "/playwright";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PlaywrightBuilder() : this(new PlaywrightConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private PlaywrightBuilder(PlaywrightConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override PlaywrightConfiguration DockerResourceConfiguration { get; }
+
+ public override PlaywrightContainer Build()
+ {
+ Validate();
+ return new PlaywrightContainer(DockerResourceConfiguration);
+ }
+
+ ///
+ protected override PlaywrightBuilder Init()
+ {
+ return base.Init()
+ .WithBrowser(PlaywrightBrowser.Chrome)
+ .WithEndpointPath(PlaywrightEndpointPath)
+ .WithBrowserPort(PlaywrightPort)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilMessageIsLogged($"ws://.*:{PlaywrightPort}{PlaywrightEndpointPath}"));
+ }
+
+ public PlaywrightBuilder WithBrowser(PlaywrightBrowser playwrightBrowser)
+ {
+ return WithImage(playwrightBrowser.Image);
+ }
+
+ ///
+ /// Sets the BROWSER WS ENDPOINT.
+ ///
+ /// The BROWSER WS ENDPOINT.
+ /// A configured instance of .
+ public PlaywrightBuilder WithEndpointPath(string endpointPath)
+ {
+ return Merge(DockerResourceConfiguration, new PlaywrightConfiguration(endpoint: endpointPath))
+ .WithEnvironment("BROWSER_WS_ENDPOINT", endpointPath);
+ }
+
+ ///
+ /// Sets the BROWSER WS PORT.
+ ///
+ /// The BROWSER WS PORT.
+ /// A configured instance of .
+ public PlaywrightBuilder WithBrowserPort(int port, bool assignRandomHostPort=false)
+ {
+ return Merge(DockerResourceConfiguration, new PlaywrightConfiguration(port: port))
+ .WithEnvironment("BROWSER_PORT", port.ToString())
+ .WithPortBinding(port, assignRandomHostPort);
+ }
+
+
+
+ ///
+ protected override PlaywrightBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new PlaywrightConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override PlaywrightBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new PlaywrightConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override PlaywrightBuilder Merge(PlaywrightConfiguration oldValue, PlaywrightConfiguration newValue)
+ {
+ return new PlaywrightBuilder(new PlaywrightConfiguration(oldValue, newValue));
+ }
+}
diff --git a/src/Testcontainers.Playwright/PlaywrightConfiguration.cs b/src/Testcontainers.Playwright/PlaywrightConfiguration.cs
new file mode 100644
index 000000000..e7dc16a20
--- /dev/null
+++ b/src/Testcontainers.Playwright/PlaywrightConfiguration.cs
@@ -0,0 +1,70 @@
+namespace Testcontainers.Playwright;
+
+///
+[PublicAPI]
+public class PlaywrightConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Playwright endpoint.
+ public PlaywrightConfiguration(string endpoint = null,
+ int? port = null)
+ {
+ Endpoint = endpoint;
+ Port = port;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public PlaywrightConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public PlaywrightConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public PlaywrightConfiguration(PlaywrightConfiguration resourceConfiguration)
+ : this(new PlaywrightConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public PlaywrightConfiguration(PlaywrightConfiguration oldValue, PlaywrightConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ Endpoint = BuildConfiguration.Combine(oldValue.Endpoint, newValue.Endpoint);
+ }
+
+
+ ///
+ /// Gets the Playwright endpoint.
+ ///
+ public string Endpoint { get; }
+
+
+ ///
+ /// Gets the Playwright port.
+ ///
+ public int? Port { get; }
+}
diff --git a/src/Testcontainers.Playwright/PlaywrightContainer.cs b/src/Testcontainers.Playwright/PlaywrightContainer.cs
new file mode 100644
index 000000000..c86007cae
--- /dev/null
+++ b/src/Testcontainers.Playwright/PlaywrightContainer.cs
@@ -0,0 +1,14 @@
+namespace Testcontainers.Playwright;
+
+///
+[PublicAPI]
+public class PlaywrightContainer : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public PlaywrightContainer(PlaywrightConfiguration configuration) : base(configuration)
+ {
+ }
+}
diff --git a/src/Testcontainers.Playwright/Testcontainers.Playwright.csproj b/src/Testcontainers.Playwright/Testcontainers.Playwright.csproj
new file mode 100644
index 000000000..2e29f8592
--- /dev/null
+++ b/src/Testcontainers.Playwright/Testcontainers.Playwright.csproj
@@ -0,0 +1,12 @@
+
+
+ net6.0;net8.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
diff --git a/src/Testcontainers.Playwright/Usings.cs b/src/Testcontainers.Playwright/Usings.cs
new file mode 100644
index 000000000..00e88cf71
--- /dev/null
+++ b/src/Testcontainers.Playwright/Usings.cs
@@ -0,0 +1,16 @@
+global using System;
+global using System.IO;
+global using System.Linq;
+global using System.Net.Http;
+global using System.Text.Json;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using DotNet.Testcontainers.Images;
+global using DotNet.Testcontainers.Networks;
+global using JetBrains.Annotations;
+global using Microsoft.Extensions.Logging.Abstractions;
\ No newline at end of file
diff --git a/tests/Testcontainers.Playwright.Tests/GlobalUsings.cs b/tests/Testcontainers.Playwright.Tests/GlobalUsings.cs
new file mode 100644
index 000000000..40c3c0c7c
--- /dev/null
+++ b/tests/Testcontainers.Playwright.Tests/GlobalUsings.cs
@@ -0,0 +1,3 @@
+global using Xunit;
+global using System;
+global using System.Threading.Tasks;
diff --git a/tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs b/tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs
new file mode 100644
index 000000000..235d423c4
--- /dev/null
+++ b/tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs
@@ -0,0 +1,28 @@
+namespace Testcontainers.Playwright.Tests;
+
+public class PlaywrightContainerTest : IClassFixture
+{
+ private readonly Uri _helloWorldBaseAddress;
+
+ public PlaywrightContainerTest(TestInitializer testInitializer)
+ {
+ _helloWorldBaseAddress = testInitializer._helloWorldBaseAddress;
+ }
+
+ [Fact]
+ public async Task HeadingElementReturnsHelloWorld()
+ {
+ // Given
+ var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
+ var browser = await playwright.Chromium.ConnectAsync($"ws://localhost:63333/playwright");
+ var page = await browser.NewPageAsync();
+
+ // When
+ await page.GotoAsync(_helloWorldBaseAddress.ToString());
+ var headingElement = await page.QuerySelectorAsync("h1");
+ var headingElementText = await headingElement.InnerTextAsync();
+
+ // Then
+ Assert.Equal("Hello world", headingElementText);
+ }
+}
diff --git a/tests/Testcontainers.Playwright.Tests/TestInitializer.cs b/tests/Testcontainers.Playwright.Tests/TestInitializer.cs
new file mode 100644
index 000000000..3682e438d
--- /dev/null
+++ b/tests/Testcontainers.Playwright.Tests/TestInitializer.cs
@@ -0,0 +1,75 @@
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Testcontainers.Playwright.Tests
+{
+ public class TestInitializer : IAsyncLifetime
+ {
+ internal readonly Uri _helloWorldBaseAddress = new UriBuilder(Uri.UriSchemeHttp, "hello-world-container", 8080).Uri;
+ private IContainer _helloWorldContainer;
+ private PlaywrightContainer _playwrightContainer;
+
+ private const string PlaywrightContainerName = "testplaywright";
+
+ public async Task InitializeAsync()
+ {
+ var network = CreateNetwork();
+
+ _helloWorldContainer = CreateHelloWorldContainer(network);
+ _playwrightContainer = CreatePlaywrightContainer(network);
+
+ await StartContainersAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await DisposeContainersAsync();
+ }
+
+ private INetwork CreateNetwork()
+ {
+ return new NetworkBuilder()
+ .WithName(Guid.NewGuid().ToString("D"))
+ .WithDriver(NetworkDriver.Bridge)
+ .WithCleanUp(true)
+ .Build();
+ }
+
+ private IContainer CreateHelloWorldContainer(INetwork network)
+ {
+ return new ContainerBuilder()
+ .WithImage("testcontainers/helloworld:1.1.0")
+ .WithNetwork(network)
+ .WithNetworkAliases(_helloWorldBaseAddress.Host)
+ .WithPortBinding(_helloWorldBaseAddress.Port, true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
+ request.ForPath("/").ForPort(Convert.ToUInt16(_helloWorldBaseAddress.Port))))
+ .Build();
+ }
+
+ private PlaywrightContainer CreatePlaywrightContainer(INetwork network)
+ {
+ return new PlaywrightBuilder()
+ .WithNetwork(network)
+ .WithName(PlaywrightContainerName)
+ .WithNetworkAliases(PlaywrightContainerName)
+ .WithPortBinding(63333, 53333)
+ .WithBrowser(PlaywrightBrowser.Chromium)
+ .Build();
+ }
+
+ private async Task StartContainersAsync()
+ {
+ await _helloWorldContainer.StartAsync();
+ await _playwrightContainer.StartAsync();
+ }
+
+ private async Task DisposeContainersAsync()
+ {
+ await _playwrightContainer.DisposeAsync();
+ await _helloWorldContainer.DisposeAsync();
+ }
+ }
+}
diff --git a/tests/Testcontainers.Playwright.Tests/Testcontainers.Playwright.Tests.csproj b/tests/Testcontainers.Playwright.Tests/Testcontainers.Playwright.Tests.csproj
new file mode 100644
index 000000000..b5cdbad07
--- /dev/null
+++ b/tests/Testcontainers.Playwright.Tests/Testcontainers.Playwright.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+ net8.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+