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 + + + + + + + + + + + + + + + + + + + +