diff --git a/Directory.Packages.props b/Directory.Packages.props index df5711266..eab74b4be 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -68,6 +68,7 @@ + diff --git a/Testcontainers.sln b/Testcontainers.sln index f6eddf7a2..8338ec57a 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -102,6 +102,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Papercut", "src\Testcontainers.Papercut\Testcontainers.Papercut.csproj", "{B2608563-8EE4-49AA-A9A0-B1614486AEEF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Playwright", "src\Testcontainers.Playwright\Testcontainers.Playwright.csproj", "{7F91A202-4F07-470D-881C-9A46A700DCA5}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql", "src\Testcontainers.PostgreSql\Testcontainers.PostgreSql.csproj", "{8AB91636-9055-4900-A72A-7CFFACDFDBF0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "src\Testcontainers.PubSub\Testcontainers.PubSub.csproj", "{E6642255-667D-476B-B584-089AA5E6C0B1}" @@ -230,6 +232,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Platform.Lin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Platform.Windows.Tests", "tests\Testcontainers.Platform.Windows.Tests\Testcontainers.Platform.Windows.Tests.csproj", "{3E55CBE8-AFE8-426D-9470-49D63CD1051C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Playwright.Tests", "tests\Testcontainers.Playwright.Tests\Testcontainers.Playwright.Tests.csproj", "{DE065875-1B7B-4E20-9332-C88C8437333D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql.Tests", "tests\Testcontainers.PostgreSql.Tests\Testcontainers.PostgreSql.Tests.csproj", "{56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub.Tests", "tests\Testcontainers.PubSub.Tests\Testcontainers.PubSub.Tests.csproj", "{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}" @@ -434,6 +438,10 @@ Global {B2608563-8EE4-49AA-A9A0-B1614486AEEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2608563-8EE4-49AA-A9A0-B1614486AEEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2608563-8EE4-49AA-A9A0-B1614486AEEF}.Release|Any CPU.Build.0 = Release|Any CPU + {7F91A202-4F07-470D-881C-9A46A700DCA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F91A202-4F07-470D-881C-9A46A700DCA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F91A202-4F07-470D-881C-9A46A700DCA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F91A202-4F07-470D-881C-9A46A700DCA5}.Release|Any CPU.Build.0 = Release|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -690,6 +698,10 @@ Global {3E55CBE8-AFE8-426D-9470-49D63CD1051C}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E55CBE8-AFE8-426D-9470-49D63CD1051C}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E55CBE8-AFE8-426D-9470-49D63CD1051C}.Release|Any CPU.Build.0 = Release|Any CPU + {DE065875-1B7B-4E20-9332-C88C8437333D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE065875-1B7B-4E20-9332-C88C8437333D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE065875-1B7B-4E20-9332-C88C8437333D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE065875-1B7B-4E20-9332-C88C8437333D}.Release|Any CPU.Build.0 = Release|Any CPU {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -805,6 +817,7 @@ Global {49051DBC-6B80-4412-8505-BC2764A877BD} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {596EAFC1-0496-495C-B382-D57415FA456A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {B2608563-8EE4-49AA-A9A0-B1614486AEEF} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {7F91A202-4F07-470D-881C-9A46A700DCA5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {27D46863-65B9-4934-B3C8-2383B217A477} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -869,6 +882,7 @@ Global {F03FA970-BE2B-4AE2-96FE-7E1F805CEA20} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {DA1D7ADE-452C-4369-83CC-56289176EACD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {3E55CBE8-AFE8-426D-9470-49D63CD1051C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {DE065875-1B7B-4E20-9332-C88C8437333D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D05FCB31-793E-43E0-BD6C-077013AE9113} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/docs/modules/index.md b/docs/modules/index.md index bf029294b..fcd99f51f 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -60,6 +60,7 @@ await moduleNameContainer.StartAsync(); | OpenSearch | `opensearchproject/opensearch:2.12.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.OpenSearch) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.OpenSearch) | | Oracle | `gvenzl/oracle-xe:21.3.0-slim-faststart` | [NuGet](https://www.nuget.org/packages/Testcontainers.Oracle) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Oracle) | | Papercut | `changemakerstudiosus/papercut-smtp:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.Papercut) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Papercut) | +| Playwright | `mcr.microsoft.com/playwright:v1.55.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.Playwright) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Playwright) | | PostgreSQL | `postgres:15.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.PostgreSql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PostgreSql) | | PubSub | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.PubSub) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.PubSub) | | Pulsar | `apachepulsar/pulsar:3.0.6` | [NuGet](https://www.nuget.org/packages/Testcontainers.Pulsar) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Pulsar) | diff --git a/docs/modules/playwright.md b/docs/modules/playwright.md new file mode 100644 index 000000000..6c2bb3f74 --- /dev/null +++ b/docs/modules/playwright.md @@ -0,0 +1,47 @@ +# Playwright + +[Playwright](https://playwright.dev/) is a framework for web testing and automation. It allows testing across all modern rendering engines including Chromium, WebKit, and Firefox with a single API. This module provides pre-configured browser containers for automated testing. + +Add the following dependency to your project file: + +```shell title="NuGet" +dotnet add package Testcontainers.Playwright +``` + +You can start a Playwright container instance from any .NET application. To create and start a container instance with the default configuration, use the module-specific builder as shown below: + +=== "Start a Playwright container" + ```csharp + var playwrightContainer = new PlaywrightBuilder().Build(); + await playwrightContainer.StartAsync(); + ``` + +This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method. + +This example demonstrates the Playwright container accessing a web site running inside another container (using the [`testcontainers/helloworld`](https://github.com/testcontainers/helloworld) image). Both containers are assigned to a shared network (see the [Network configuration](#network-configuration) section) to enable communication between them. + +=== "Usage Example" + ```csharp + --8<-- "tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs:UsePlaywrightContainer" + ``` + +The test example uses the following NuGet dependencies: + +=== "Package References" + ```xml + --8<-- "tests/Testcontainers.Playwright.Tests/Testcontainers.Playwright.Tests.csproj:PackageReferences" + ``` + +To execute the tests, use the command `dotnet test` from a terminal. + +--8<-- "docs/modules/_call_out_test_projects.txt" + +## Network configuration + +The Playwright container is configured with a network that can be shared with other containers. This is useful when testing applications that need to communicate with other services. Use the `GetNetwork()` method to access the container's network: + +```csharp +var helloWorldContainer = new ContainerBuilder() + .WithNetwork(playwrightContainer.GetNetwork()) + .Build(); +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index af21de188..a13468237 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -60,6 +60,7 @@ nav: - modules/mssql.md - modules/neo4j.md - modules/opensearch.md + - modules/playwright.md - modules/postgres.md - modules/qdrant.md - modules/rabbitmq.md diff --git a/src/Testcontainers.Playwright/.editorconfig b/src/Testcontainers.Playwright/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Playwright/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Playwright/PlaywrightBuilder.cs b/src/Testcontainers.Playwright/PlaywrightBuilder.cs new file mode 100644 index 000000000..5711fc079 --- /dev/null +++ b/src/Testcontainers.Playwright/PlaywrightBuilder.cs @@ -0,0 +1,73 @@ +namespace Testcontainers.Playwright; + +/// +[PublicAPI] +public sealed class PlaywrightBuilder : ContainerBuilder +{ + public const string PlaywrightNetworkAlias = "standalone-container"; + + public const string PlaywrightImage = "mcr.microsoft.com/playwright:v1.55.1"; + + public const ushort PlaywrightPort = 8080; + + /// + /// 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() + .WithImage(PlaywrightImage) + .WithNetwork(new NetworkBuilder().Build()) + .WithNetworkAliases(PlaywrightNetworkAlias) + .WithPortBinding(PlaywrightPort, true) + .WithEntrypoint("/bin/sh", "-c") + // Extract the Playwright version from the container at startup. + .WithCommand("npx -y playwright@$(sed --quiet 's/.*\\\"driverVersion\\\": *\"\\([^\"]*\\)\".*/\\1/p' ms-playwright/.docker-info) run-server --port " + PlaywrightPort) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Listening on ws://localhost:8080/")); + } + + /// + 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)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Playwright/PlaywrightConfiguration.cs b/src/Testcontainers.Playwright/PlaywrightConfiguration.cs new file mode 100644 index 000000000..acf821c4d --- /dev/null +++ b/src/Testcontainers.Playwright/PlaywrightConfiguration.cs @@ -0,0 +1,53 @@ +namespace Testcontainers.Playwright; + +/// +[PublicAPI] +public sealed class PlaywrightConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public PlaywrightConfiguration() + { + } + + /// + /// 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) + { + } +} \ No newline at end of file diff --git a/src/Testcontainers.Playwright/PlaywrightContainer.cs b/src/Testcontainers.Playwright/PlaywrightContainer.cs new file mode 100644 index 000000000..1c9df4eb8 --- /dev/null +++ b/src/Testcontainers.Playwright/PlaywrightContainer.cs @@ -0,0 +1,36 @@ +namespace Testcontainers.Playwright; + +/// +[PublicAPI] +public sealed class PlaywrightContainer : DockerContainer +{ + private readonly PlaywrightConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public PlaywrightContainer(PlaywrightConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + + /// + /// Gets the Playwright connection string. + /// + /// The Playwright connection string. + public string GetConnectionString() + { + return new UriBuilder("ws", Hostname, GetMappedPublicPort(PlaywrightBuilder.PlaywrightPort)).ToString(); + } + + /// + /// Gets the Playwright network. + /// + /// The Playwright network. + public INetwork GetNetwork() + { + return _configuration.Networks.Single(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Playwright/Testcontainers.Playwright.csproj b/src/Testcontainers.Playwright/Testcontainers.Playwright.csproj new file mode 100644 index 000000000..9a25b9c4d --- /dev/null +++ b/src/Testcontainers.Playwright/Testcontainers.Playwright.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Playwright/Usings.cs b/src/Testcontainers.Playwright/Usings.cs new file mode 100644 index 000000000..f1ac1e74c --- /dev/null +++ b/src/Testcontainers.Playwright/Usings.cs @@ -0,0 +1,9 @@ +global using System; +global using System.Linq; +global using Docker.DotNet.Models; +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; \ No newline at end of file diff --git a/tests/Testcontainers.Playwright.Tests/.editorconfig b/tests/Testcontainers.Playwright.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Playwright.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Playwright.Tests/.runs-on b/tests/Testcontainers.Playwright.Tests/.runs-on new file mode 100644 index 000000000..d0395e498 --- /dev/null +++ b/tests/Testcontainers.Playwright.Tests/.runs-on @@ -0,0 +1 @@ +ubuntu-24.04 \ No newline at end of file diff --git a/tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs b/tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs new file mode 100644 index 000000000..d9212eb5f --- /dev/null +++ b/tests/Testcontainers.Playwright.Tests/PlaywrightContainerTest.cs @@ -0,0 +1,89 @@ +namespace Testcontainers.Playwright; + +public abstract class PlaywrightContainerTest : IAsyncLifetime +{ + private readonly Uri _helloWorldBaseAddress = new UriBuilder(Uri.UriSchemeHttp, "hello-world-container", 8080).Uri; + + private readonly IContainer _helloWorldContainer; + + private readonly PlaywrightContainer _playwrightContainer; + + private PlaywrightContainerTest(PlaywrightContainer playwrightContainer) + { + _helloWorldContainer = new ContainerBuilder() + .WithImage(CommonImages.HelloWorld) + .WithNetwork(playwrightContainer.GetNetwork()) + .WithNetworkAliases(_helloWorldBaseAddress.Host) + .WithPortBinding(_helloWorldBaseAddress.Port, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPath("/").ForPort(Convert.ToUInt16(_helloWorldBaseAddress.Port)))) + .Build(); + + _playwrightContainer = playwrightContainer; + } + + public async ValueTask InitializeAsync() + { + await _playwrightContainer.StartAsync() + .ConfigureAwait(false); + + await _helloWorldContainer.StartAsync() + .ConfigureAwait(false); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore() + .ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + // # --8<-- [start:UsePlaywrightContainer] + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task HeadingElementReturnsHelloWorld() + { + // Given + var playwright = await Microsoft.Playwright.Playwright.CreateAsync() + .ConfigureAwait(true); + + var browser = await playwright.Chromium.ConnectAsync(_playwrightContainer.GetConnectionString()) + .ConfigureAwait(true); + + var page = await browser.NewPageAsync() + .ConfigureAwait(true); + + // When + await page.GotoAsync(_helloWorldBaseAddress.ToString()) + .ConfigureAwait(true); + + var headingElement = await page.QuerySelectorAsync("h1") + .ConfigureAwait(true); + + var headingElementText = await headingElement!.InnerTextAsync() + .ConfigureAwait(true); + + // Then + Assert.Equal("Hello world", headingElementText); + } + // # --8<-- [end:UsePlaywrightContainer] + + protected virtual async ValueTask DisposeAsyncCore() + { + await _helloWorldContainer.DisposeAsync() + .ConfigureAwait(false); + + await _playwrightContainer.DisposeAsync() + .ConfigureAwait(false); + } + + [UsedImplicitly] + public sealed class PlaywrightDefaultConfiguration : PlaywrightContainerTest + { + public PlaywrightDefaultConfiguration() + : base(new PlaywrightBuilder().Build()) + { + } + } +} \ No newline at end of file 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..6a82af1cc --- /dev/null +++ b/tests/Testcontainers.Playwright.Tests/Testcontainers.Playwright.Tests.csproj @@ -0,0 +1,21 @@ + + + net9.0 + false + false + Exe + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Playwright.Tests/Usings.cs b/tests/Testcontainers.Playwright.Tests/Usings.cs new file mode 100644 index 000000000..30832d99e --- /dev/null +++ b/tests/Testcontainers.Playwright.Tests/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Commons; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Xunit; \ No newline at end of file