diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml index c6ea71c696..dab5144ddf 100644 --- a/.azure-pipelines/publish.yml +++ b/.azure-pipelines/publish.yml @@ -1,4 +1,10 @@ -trigger: none # We don't want CI builds, just a manual release process +# Run only on tags (no PRs, no branch CI) +pr: none +trigger: + tags: + include: + - '*' + parameters: - name: doRelease displayName: Push the Playwright Release to NuGet.org @@ -12,9 +18,9 @@ parameters: variables: - name: BuildConfiguration - value: 'Release' + value: 'Release' - name: TeamName - value: Playwright + value: 'Playwright' resources: repositories: @@ -44,7 +50,7 @@ extends: signing: enabled: true signType: real - templateContext: + signWithProd: true outputs: - output: pipelineArtifact displayName: 'Publish Artifact' @@ -77,25 +83,30 @@ extends: src/Playwright/Playwright.csproj; src/Playwright.NUnit/Playwright.NUnit.csproj; src/Playwright.MSTest/Playwright.MSTest.csproj; + src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj; src/Playwright.Xunit/Playwright.Xunit.csproj; + src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj; src/Playwright.TestAdapter/Playwright.TestAdapter.csproj; packDirectory: '$(Build.ArtifactStagingDirectory)/nuget' versioningScheme: 'off' - task: 1ES.PublishNuget@1 displayName: Publish Microsoft.Playwright{NUnit,MSTest,Xunit,TestAdapter} - condition: eq('${{parameters.doRelease}}', true) + condition: or( + eq('${{ parameters.doRelease }}', true), + startsWith(variables['Build.SourceBranch'], 'refs/tags/') + ) inputs: useDotNetTask: false # The reason for the 1.* after the package name is that we know the end of the package name in order to not # match with e.g. Microsoft.Playwright.CLI when we only want to match Microsoft.Playwright. # Semicolon separated as per https://portal.microsofticm.com/imp/v5/incidents/details/467483180/summary - packagesToPush: $(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.MSTest.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.Xunit.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.NUnit.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.TestAdapter.1.*.nupkg + packagesToPush: $(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.MSTest.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.MSTest.v4.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.Xunit.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.Xunit.v3.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.NUnit.1.*.nupkg;$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.TestAdapter.1.*.nupkg packageParentPath: '$(Build.ArtifactStagingDirectory)' nuGetFeedType: external publishFeedCredentials: 'NuGet-Playwright' - task: 1ES.PublishNuget@1 displayName: Publish Microsoft.Playwright.CLI - condition: eq('${{parameters.doReleaseCLI}}', true) + condition: eq('${{ parameters.doReleaseCLI }}', true) inputs: useDotNetTask: false packagesToPush: '$(Build.ArtifactStagingDirectory)/nuget/Microsoft.Playwright.CLI.1.*.nupkg' diff --git a/.config/1espt/PipelineAutobaseliningConfig.yml b/.config/1espt/PipelineAutobaseliningConfig.yml new file mode 100644 index 0000000000..3c650dcb4e --- /dev/null +++ b/.config/1espt/PipelineAutobaseliningConfig.yml @@ -0,0 +1,23 @@ +## DO NOT MODIFY THIS FILE MANUALLY. This is part of auto-baselining from 1ES Pipeline Templates. Go to [https://aka.ms/1espt-autobaselining] for more details. + +pipelines: + 14588: + retail: + source: + credscan: + lastModifiedDate: 2025-08-12 + eslint: + lastModifiedDate: 2025-08-12 + psscriptanalyzer: + lastModifiedDate: 2025-08-12 + armory: + lastModifiedDate: 2025-08-12 + accessibilityinsights: + lastModifiedDate: 2025-08-12 + binary: + credscan: + lastModifiedDate: 2025-08-12 + binskim: + lastModifiedDate: 2025-08-12 + spotbugs: + lastModifiedDate: 2025-08-12 diff --git a/.config/guardian/.gdnbaselines b/.config/guardian/.gdnbaselines new file mode 100644 index 0000000000..a56e1789cd --- /dev/null +++ b/.config/guardian/.gdnbaselines @@ -0,0 +1,113 @@ +{ + "properties": { + "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines" + }, + "version": "1.0.0", + "baselines": { + "default": { + "name": "default", + "createdDate": "2025-08-12 12:02:09Z", + "lastUpdatedDate": "2025-08-12 12:02:09Z" + } + }, + "results": { + "b2c126d95a9b96964d18d3a60b8100823a08da6340de51db534197aea137861b": { + "signature": "b2c126d95a9b96964d18d3a60b8100823a08da6340de51db534197aea137861b", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/TestServer/key.pfx", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + }, + "109e68661121356b830572b5e4140939c7da48a90a6824db38785bd42f904d03": { + "signature": "109e68661121356b830572b5e4140939c7da48a90a6824db38785bd42f904d03", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/assets/client-certificates/server/server_key.pem", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + }, + "a7f17efaa8efc6de4a0288d154928d8ae13dcd23350dc10bcb2ae54dcaf90878": { + "signature": "a7f17efaa8efc6de4a0288d154928d8ae13dcd23350dc10bcb2ae54dcaf90878", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/assets/client-certificates/client/localhost/localhost.key", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + }, + "fd3fe59c06490e9b3f4cc72ed4cc7e4110ee3676bbfc2efe9c617566701923bf": { + "signature": "fd3fe59c06490e9b3f4cc72ed4cc7e4110ee3676bbfc2efe9c617566701923bf", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/assets/client-certificates/client/self-signed/key.pem", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + }, + "9409e383d7ec985e0e5793d49c243881276800040b9e6686b1b464e6c92bd197": { + "signature": "9409e383d7ec985e0e5793d49c243881276800040b9e6686b1b464e6c92bd197", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/assets/client-certificates/client/trusted/cert-legacy.pfx", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + }, + "a9f444c2c883c71914cfa934ab765cf6cef61b629195695750493f8198e01b8e": { + "signature": "a9f444c2c883c71914cfa934ab765cf6cef61b629195695750493f8198e01b8e", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/assets/client-certificates/client/trusted/cert.pfx", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + }, + "f65b0b3a90f82519d4a24aaa87ae2ec94a812c6fb10562045839de0e9ab1ea66": { + "signature": "f65b0b3a90f82519d4a24aaa87ae2ec94a812c6fb10562045839de0e9ab1ea66", + "alternativeSignatures": [], + "target": "src/Playwright.Tests/assets/client-certificates/client/trusted/key.pem", + "line": 1, + "memberOf": [ + "default" + ], + "tool": "credscan", + "ruleId": "CSCAN-GENERAL0020", + "createdDate": "2025-08-12 12:02:09Z", + "expirationDate": "2026-01-29 12:52:02Z", + "justification": "This error is baselined with an expiration date of 180 days from 2025-08-12 12:52:02Z" + } + } +} \ No newline at end of file diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index 8afcd9b802..bfbe0bbf10 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -12,7 +12,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/nuget-package-tests.yml b/.github/workflows/nuget-package-tests.yml index bd214e827b..01be480991 100644 --- a/.github/workflows/nuget-package-tests.yml +++ b/.github/workflows/nuget-package-tests.yml @@ -17,7 +17,7 @@ jobs: matrix: os: [windows-latest, ubuntu-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 126b262b84..51635ba662 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -15,7 +15,7 @@ jobs: contents: read # This is required for actions/checkout to succeed environment: Docker steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Azure login uses: azure/login@v2 with: diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index c1e9aead4a..3a36f3438c 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -26,7 +26,7 @@ jobs: flavor: [jammy, noble] runs-on: [ubuntu-24.04, ubuntu-24.04-arm] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60b18595c0..348c504875 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: browser: [chromium, firefox, webkit] os: [windows-latest, ubuntu-latest, macos-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/tests_harness.yml b/.github/workflows/tests_harness.yml index d9cc0d23f6..d1329404b0 100644 --- a/.github/workflows/tests_harness.yml +++ b/.github/workflows/tests_harness.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: diff --git a/.github/workflows/validate-nuget-packages.yml b/.github/workflows/validate-nuget-packages.yml index 9140b5a16f..4cb06c1fd7 100644 --- a/.github/workflows/validate-nuget-packages.yml +++ b/.github/workflows/validate-nuget-packages.yml @@ -12,7 +12,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: diff --git a/README.md b/README.md index b885a5d42d..88b3268d52 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 139.0.7258.5 | ✅ | ✅ | ✅ | +| Chromium 140.0.7339.16 | ✅ | ✅ | ✅ | | WebKit 26.0 | ✅ | ✅ | ✅ | -| Firefox 140.0.2 | ✅ | ✅ | ✅ | +| Firefox 141.0 | ✅ | ✅ | ✅ | Playwright for .NET is the official language port of [Playwright](https://playwright.dev), the library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**. diff --git a/src/Common/Version.props b/src/Common/Version.props index cb0728d9b5..d58172cf1d 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -1,8 +1,8 @@ - 1.54.0 + 1.55.0 $(AssemblyVersion) - 1.54.1 + 1.55.0-beta-1756314050000 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.CLI/Playwright.CLI.csproj b/src/Playwright.CLI/Playwright.CLI.csproj index 91c522fbe2..b16c35413e 100644 --- a/src/Playwright.CLI/Playwright.CLI.csproj +++ b/src/Playwright.CLI/Playwright.CLI.csproj @@ -34,6 +34,7 @@ MIT + README.md @@ -49,5 +50,6 @@ + diff --git a/src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj b/src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj new file mode 100644 index 0000000000..0b6ccd8420 --- /dev/null +++ b/src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj @@ -0,0 +1,52 @@ + + + + Microsoft.Playwright.MSTest.v4 + Microsoft.Playwright.MSTest.v4 + A set of helpers and fixtures to enable using Playwright in MSTest 4 tests. + + Playwright enables reliable end-to-end testing for modern web apps. This package brings in additional helpers + and fixtures to enable using it within MSTest. + + icon.png + netstandard2.0 + true + true + Microsoft.Playwright.MSTest + 0.0.0 + True + Microsoft.Playwright.MSTest.v4 + ./nupkg + true + enable + README.md + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + %(RecursiveDir)%(FileName)%(Extension) + + + diff --git a/src/Playwright.MSTest/Playwright.MSTest.csproj b/src/Playwright.MSTest/Playwright.MSTest.csproj index d9f257d80e..b00180aff4 100644 --- a/src/Playwright.MSTest/Playwright.MSTest.csproj +++ b/src/Playwright.MSTest/Playwright.MSTest.csproj @@ -19,6 +19,7 @@ ./nupkg true enable + README.md true 1.52.0 @@ -43,5 +44,6 @@ + diff --git a/src/Playwright.MSTest/WorkerAwareTest.cs b/src/Playwright.MSTest/WorkerAwareTest.cs index 0bea2243c9..d333c5523b 100644 --- a/src/Playwright.MSTest/WorkerAwareTest.cs +++ b/src/Playwright.MSTest/WorkerAwareTest.cs @@ -10,7 +10,7 @@ namespace Microsoft.Playwright.MSTest; public class WorkerAwareTest { - public TestContext TestContext { get; set; } = null!; + public virtual TestContext TestContext { get; set; } = null!; private static readonly ConcurrentStack _allWorkers = new(); private Worker _currentWorker = null!; diff --git a/src/Playwright.NUnit/Playwright.NUnit.csproj b/src/Playwright.NUnit/Playwright.NUnit.csproj index b3aa923280..992a676455 100644 --- a/src/Playwright.NUnit/Playwright.NUnit.csproj +++ b/src/Playwright.NUnit/Playwright.NUnit.csproj @@ -19,6 +19,7 @@ ./nupkg true enable + README.md true 1.52.0 @@ -43,5 +44,6 @@ + diff --git a/src/Playwright.TestAdapter/Playwright.TestAdapter.csproj b/src/Playwright.TestAdapter/Playwright.TestAdapter.csproj index e009cc0980..873863f844 100644 --- a/src/Playwright.TestAdapter/Playwright.TestAdapter.csproj +++ b/src/Playwright.TestAdapter/Playwright.TestAdapter.csproj @@ -18,6 +18,7 @@ ./nupkg true enable + README.md true 1.52.0 @@ -40,5 +41,6 @@ + diff --git a/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs b/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs index 0b6b8872a9..58ee2594b1 100644 --- a/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs +++ b/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs @@ -23,6 +23,7 @@ */ using System; +using System.Text.Json; using System.Xml; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -35,6 +36,22 @@ public class PlaywrightSettingsProvider : ISettingsProvider { private static PlaywrightSettingsXml? _settings = null!; + public static void LoadViaEnvIfNeeded() + { + if (_settings == null) + { + var settings = Environment.GetEnvironmentVariable("PW_INTERNAL_ADAPTER_SETTINGS"); + if (!string.IsNullOrEmpty(settings)) + { + _settings = JsonSerializer.Deserialize(settings); + } + else + { + _settings = new PlaywrightSettingsXml(); + } + } + } + public static string BrowserName { get @@ -102,5 +119,10 @@ private static void ValidateBrowserName(string browserName, string fromText, str } public void Load(XmlReader reader) - => _settings = new PlaywrightSettingsXml(reader); + { + // NOTE: ISettingsProvider::Load is not called when there are no runsettings (either file or passed via command line). + _settings = new PlaywrightSettingsXml(reader); + Environment.SetEnvironmentVariable("PW_INTERNAL_ADAPTER_SETTINGS", JsonSerializer.Serialize(_settings)); + } } + diff --git a/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs b/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs index dea4d983da..6e9371d982 100644 --- a/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs +++ b/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs @@ -35,6 +35,10 @@ namespace Microsoft.Playwright.TestAdapter; public class PlaywrightSettingsXml { + public PlaywrightSettingsXml() + { + } + public PlaywrightSettingsXml(XmlReader reader) { // Skip Playwright root Element @@ -159,10 +163,10 @@ private static object ParseAsJson(string value, Type type) return JsonSerializer.Deserialize(value.Replace('\'', '"'), type)!; } - public BrowserTypeLaunchOptions? LaunchOptions { get; private set; } - public string? BrowserName { get; private set; } - public bool? Headless { get; private set; } - public float? ExpectTimeout { get; private set; } - public int? Retries { get; private set; } + public BrowserTypeLaunchOptions? LaunchOptions { get; set; } + public string? BrowserName { get; set; } + public bool? Headless { get; set; } + public float? ExpectTimeout { get; set; } + public int? Retries { get; set; } } diff --git a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj index a553b29050..2bc474ac12 100644 --- a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj +++ b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj @@ -11,13 +11,18 @@ - + + + + + + @@ -26,6 +31,11 @@ + + + + + diff --git a/src/Playwright.TestingHarnessTest/package-lock.json b/src/Playwright.TestingHarnessTest/package-lock.json index f4aea272a6..3f13a54fad 100644 --- a/src/Playwright.TestingHarnessTest/package-lock.json +++ b/src/Playwright.TestingHarnessTest/package-lock.json @@ -7,19 +7,19 @@ "": { "name": "playwright.testingharnesstest", "devDependencies": { - "@playwright/test": "1.54.1", + "@playwright/test": "1.55.0-beta-1756314050000", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } }, "node_modules/@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "version": "1.55.0-beta-1756314050000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-beta-1756314050000.tgz", + "integrity": "sha512-Iqiy5YO7fNGaUcjZSBn9NHnAjEIROVsUzfbHKbg28WDudgMu5KTpk3nh39WTbRJoamSgfECGmuMTJMuwgyfgWw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.54.1" + "playwright": "1.55.0-beta-1756314050000" }, "bin": { "playwright": "cli.js" @@ -77,13 +77,13 @@ } }, "node_modules/playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "version": "1.55.0-beta-1756314050000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-beta-1756314050000.tgz", + "integrity": "sha512-dJJmaVqz+z5UqvU8S4yex796OSGqyp4ej7py+T5t6YjN6NeHW8gqHy8uaDrxKs7i7PXn+60LCbhRIM/rCN2NIA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.54.1" + "playwright-core": "1.55.0-beta-1756314050000" }, "bin": { "playwright": "cli.js" @@ -96,9 +96,9 @@ } }, "node_modules/playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "version": "1.55.0-beta-1756314050000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-beta-1756314050000.tgz", + "integrity": "sha512-jgAGIr1Vbgkb984uxj5JBngw53Gygp1u6PNF4X52+Wo4Q0PfEj4mUwgomm+AC3g8LEze4cR7Lk9+oQRUq7as2Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -124,12 +124,12 @@ }, "dependencies": { "@playwright/test": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", - "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "version": "1.55.0-beta-1756314050000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0-beta-1756314050000.tgz", + "integrity": "sha512-Iqiy5YO7fNGaUcjZSBn9NHnAjEIROVsUzfbHKbg28WDudgMu5KTpk3nh39WTbRJoamSgfECGmuMTJMuwgyfgWw==", "dev": true, "requires": { - "playwright": "1.54.1" + "playwright": "1.55.0-beta-1756314050000" } }, "@types/node": { @@ -158,19 +158,19 @@ "optional": true }, "playwright": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", - "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "version": "1.55.0-beta-1756314050000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0-beta-1756314050000.tgz", + "integrity": "sha512-dJJmaVqz+z5UqvU8S4yex796OSGqyp4ej7py+T5t6YjN6NeHW8gqHy8uaDrxKs7i7PXn+60LCbhRIM/rCN2NIA==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.54.1" + "playwright-core": "1.55.0-beta-1756314050000" } }, "playwright-core": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", - "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "version": "1.55.0-beta-1756314050000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0-beta-1756314050000.tgz", + "integrity": "sha512-jgAGIr1Vbgkb984uxj5JBngw53Gygp1u6PNF4X52+Wo4Q0PfEj4mUwgomm+AC3g8LEze4cR7Lk9+oQRUq7as2Q==", "dev": true }, "strnum": { diff --git a/src/Playwright.TestingHarnessTest/package.json b/src/Playwright.TestingHarnessTest/package.json index 195bd6cd22..89b302912c 100644 --- a/src/Playwright.TestingHarnessTest/package.json +++ b/src/Playwright.TestingHarnessTest/package.json @@ -2,7 +2,7 @@ "name": "playwright.testingharnesstest", "private": true, "devDependencies": { - "@playwright/test": "1.54.1", + "@playwright/test": "1.55.0-beta-1756314050000", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } diff --git a/src/Playwright.TestingHarnessTest/tests/baseTest.ts b/src/Playwright.TestingHarnessTest/tests/baseTest.ts index 968eba1dc9..55e850795c 100644 --- a/src/Playwright.TestingHarnessTest/tests/baseTest.ts +++ b/src/Playwright.TestingHarnessTest/tests/baseTest.ts @@ -19,9 +19,10 @@ type RunResult = { export const test = base.extend<{ proxyServer: ProxyServer; - testMode: 'nunit' | 'mstest' | 'xunit'; + testMode: 'nunit' | 'mstest' | 'mstest.v4' | 'xunit' | 'xunit.v3'; runTest: (files: Record, command: string, env?: NodeJS.ProcessEnv) => Promise; - launchServer: ({ port: number }) => Promise; + launchServer: (options: { port: number }) => Promise; + server: SimpleServer; }>({ proxyServer: async ({}, use) => { const proxyServer = new ProxyServer(); @@ -29,6 +30,12 @@ export const test = base.extend<{ await use(proxyServer); await proxyServer.stop(); }, + server: async ({}, use) => { + const server = new SimpleServer(); + await server.listen(); + await use(server); + await server.stop(); + }, testMode: null, launchServer: async ({ playwright }, use) => { const servers: BrowserServer[] = []; @@ -183,4 +190,38 @@ class ProxyServer { } } +class SimpleServer { + private _server: http.Server; + + constructor() { + this._server = http.createServer(this.handler.bind(this)); + } + + handler(req: http.IncomingMessage, res: http.ServerResponse) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + Test Server + + +

Test Server

+

This is a simple test server for Playwright tests.

+ +`); + } + + get EMPTY_PAGE() { + return `http://127.0.0.1:${(this._server.address() as AddressInfo).port}/empty.html`; + } + + async listen() { + await new Promise(resolve => this._server.listen(0, resolve)); + } + + async stop() { + await new Promise(resolve => this._server.close(() => resolve())); + } +} + export { expect } from '@playwright/test'; diff --git a/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/mstest.spec.ts similarity index 98% rename from src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts rename to src/Playwright.TestingHarnessTest/tests/mstest.spec.ts index c1d8e1ae6b..83ba2685eb 100644 --- a/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts +++ b/src/Playwright.TestingHarnessTest/tests/mstest.spec.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { test, expect } from '../baseTest'; +import { test, expect } from './baseTest'; test.use({ testMode: 'mstest' }); @@ -223,7 +223,7 @@ test('should be able to parse BrowserName and LaunchOptions.Headless from runset expect(result.stdout).not.toContain("Headless") }); -test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer }) => { +test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer, server }) => { const result = await runTest({ 'ExampleTests.cs': ` using System; @@ -240,7 +240,7 @@ test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ ru public async Task Test() { Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); - await Page.GotoAsync("http://example.com"); + await Page.GotoAsync("${server.EMPTY_PAGE}"); } }`, '.runsettings': ` @@ -266,8 +266,8 @@ test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ ru expect(result.stdout).not.toContain("Headless"); - const { url, auth } = proxyServer.requests.find(r => r.url === 'http://example.com/')!;; - expect(url).toBe('http://example.com/'); + const { url, auth } = proxyServer.requests.find(r => r.url === server.EMPTY_PAGE)!; + expect(url).toBe(server.EMPTY_PAGE); expect(auth).toBe('user:pwd'); }); @@ -307,7 +307,7 @@ test('should be able to parse LaunchOptions.Args from runsettings', async ({ run expect(result.stdout).toContain("User-Agent: hello") }); -test('should be able to override context options', async ({ runTest }) => { +test('should be able to override context options', async ({ runTest, server }) => { const result = await runTest({ 'ExampleTests.cs': ` using System; @@ -335,7 +335,7 @@ test('should be able to override context options', async ({ runTest }) => { Assert.AreEqual("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); - var response = await Page.GotoAsync("https://example.com/"); + var response = await Page.GotoAsync("${server.EMPTY_PAGE}"); Assert.AreEqual(await response.Request.HeaderValueAsync("Kekstar"), "KekStarValue"); } diff --git a/src/Playwright.TestingHarnessTest/tests/mstest.v4.spec.ts b/src/Playwright.TestingHarnessTest/tests/mstest.v4.spec.ts new file mode 100644 index 0000000000..a4904443b1 --- /dev/null +++ b/src/Playwright.TestingHarnessTest/tests/mstest.v4.spec.ts @@ -0,0 +1,537 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { test, expect } from './baseTest'; + +test.use({ testMode: 'mstest.v4' }); + +test('should be able to forward DEBUG=pw:api env var', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + try + { + await Page.Locator("button").ClickAsync(new() { Timeout = 1_000 }); + } + catch + { + } + } + }`, + '.runsettings': ` + + + + + pw:api + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stderr).toContain("pw:api") + expect(result.stderr).toContain("element is not enabled") + expect(result.stderr).toContain("retrying click action") +}); + +test('should be able to set the browser via the runsettings file', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.WriteLine("BrowserType: " + BrowserType.Name); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + webkit + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: webkit") + expect(result.stdout).toContain("BrowserType: webkit") + expect(/User-Agent: .*WebKit.*/.test(result.stdout)).toBeTruthy() +}); + +test('should prioritize browser from env over the runsettings file', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.WriteLine("BrowserType: " + BrowserType.Name); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + webkit + + + `, + }, 'dotnet test --settings=.runsettings', { + BROWSER: 'firefox' + }); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: firefox") + expect(result.stdout).toContain("BrowserType: firefox") + expect(/User-Agent: .*Firefox.*/.test(result.stdout)).toBeTruthy() +}); + +test('should be able to make the browser headed via the env', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + }, 'dotnet test', { + HEADED: '1' + }); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: chromium") + expect(result.stdout).not.toContain("Headless") +}); + +test('should be able to parse BrowserName and LaunchOptions.Headless from runsettings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + false + + + firefox + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: firefox") + expect(result.stdout).not.toContain("Headless") +}); + +test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer, server }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + await Page.GotoAsync("${server.EMPTY_PAGE}"); + } + }`, + '.runsettings': ` + + + + chromium + + false + + ${proxyServer.listenAddr()} + user + pwd + + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + + expect(result.stdout).not.toContain("Headless"); + + const { url, auth } = proxyServer.requests.find(r => r.url === server.EMPTY_PAGE)!; + expect(url).toBe(server.EMPTY_PAGE); + expect(auth).toBe('user:pwd'); +}); + +test('should be able to parse LaunchOptions.Args from runsettings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + ['--user-agent=hello'] + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("User-Agent: hello") +}); + +test('should be able to override context options', async ({ runTest, server }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + + Assert.IsFalse(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: light)').matches")); + Assert.IsTrue(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: dark)').matches")); + + Assert.AreEqual(1920, await Page.EvaluateAsync("() => window.innerWidth")); + Assert.AreEqual(1080, await Page.EvaluateAsync("() => window.innerHeight")); + + Assert.AreEqual("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); + + var response = await Page.GotoAsync("${server.EMPTY_PAGE}"); + Assert.AreEqual(await response.Request.HeaderValueAsync("Kekstar"), "KekStarValue"); + } + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions() + { + ColorScheme = ColorScheme.Dark, + UserAgent = "Foobar", + ViewportSize = new() + { + Width = 1920, + Height = 1080 + }, + ExtraHTTPHeaders = new Dictionary { + { "Kekstar", "KekStarValue" } + } + }; + } + }`}, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); +}); + +test('should be able to override launch options', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + false + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).not.toContain("Headless"); +}); + +test.describe('Expect() timeout', () => { + test('should have 5 seconds by default', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text"); + } + }`, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("Expect \"ToHaveTextAsync\" with timeout 5000ms") + }); + + test('should be able to override it via each Expect() call', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text", new() { Timeout = 100 }); + } + }`, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("Expect \"ToHaveTextAsync\" with timeout 100ms") + }); + test('should be able to override it via the global settings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text"); + } + }`, + '.runsettings': ` + + + + 123 + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("Expect \"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 : 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); + }); +}); diff --git a/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/nunit.spec.ts similarity index 98% rename from src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts rename to src/Playwright.TestingHarnessTest/tests/nunit.spec.ts index 995ce49b3f..4d79652803 100644 --- a/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts +++ b/src/Playwright.TestingHarnessTest/tests/nunit.spec.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { test, expect } from '../baseTest'; +import { test, expect } from './baseTest'; test.use({ testMode: 'nunit' }); @@ -221,7 +221,7 @@ test('should be able to parse BrowserName and LaunchOptions.Headless from runset expect(result.stdout).not.toContain("Headless") }); -test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer }) => { +test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer, server }) => { const result = await runTest({ 'ExampleTests.cs': ` using System; @@ -237,7 +237,7 @@ test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ ru public async Task Test() { Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); - await Page.GotoAsync("http://example.com"); + await Page.GotoAsync("${server.EMPTY_PAGE}"); } }`, '.runsettings': ` @@ -263,8 +263,8 @@ test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ ru expect(result.stdout).not.toContain("Headless"); - const { url, auth } = proxyServer.requests.find(r => r.url === 'http://example.com/')!;; - expect(url).toBe('http://example.com/'); + const { url, auth } = proxyServer.requests.find(r => r.url === server.EMPTY_PAGE)!; + expect(url).toBe(server.EMPTY_PAGE); expect(auth).toBe('user:pwd'); }); @@ -303,7 +303,7 @@ test('should be able to parse LaunchOptions.Args from runsettings', async ({ run expect(result.stdout).toContain("User-Agent: hello") }); -test('should be able to override context options', async ({ runTest }) => { +test('should be able to override context options', async ({ runTest, server }) => { const result = await runTest({ 'ExampleTests.cs': ` using System; @@ -330,7 +330,7 @@ test('should be able to override context options', async ({ runTest }) => { Assert.AreEqual("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); - var response = await Page.GotoAsync("https://example.com/"); + var response = await Page.GotoAsync("${server.EMPTY_PAGE}"); Assert.AreEqual(await response.Request.HeaderValueAsync("Kekstar"), "KekStarValue"); } diff --git a/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/xunit.spec.ts similarity index 98% rename from src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts rename to src/Playwright.TestingHarnessTest/tests/xunit.spec.ts index e6c603c973..c10ba88ed5 100644 --- a/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts +++ b/src/Playwright.TestingHarnessTest/tests/xunit.spec.ts @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { test, expect } from '../baseTest'; +import { test, expect } from './baseTest'; test.use({ testMode: 'xunit' }); @@ -251,7 +251,7 @@ test('should be able to parse BrowserName and LaunchOptions.Headless from runset expect(result.stdout).not.toContain("Headless") }); -test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer }) => { +test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer, server }) => { const result = await runTest({ 'ExampleTests.cs': ` using System; @@ -275,7 +275,7 @@ test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ ru public async Task Test() { output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); - await Page.GotoAsync("http://example.com"); + await Page.GotoAsync("${server.EMPTY_PAGE}"); } }`, '.runsettings': ` @@ -301,8 +301,8 @@ test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ ru expect(result.stdout).not.toContain("Headless"); - const { url, auth } = proxyServer.requests.find(r => r.url === 'http://example.com/')!;; - expect(url).toBe('http://example.com/'); + const { url, auth } = proxyServer.requests.find(r => r.url === server.EMPTY_PAGE)!; + expect(url).toBe(server.EMPTY_PAGE); expect(auth).toBe('user:pwd'); }); @@ -349,7 +349,7 @@ test('should be able to parse LaunchOptions.Args from runsettings', async ({ run expect(result.stdout).toContain("User-Agent: hello") }); -test('should be able to override context options', async ({ runTest }) => { +test('should be able to override context options', async ({ runTest, server }) => { const result = await runTest({ 'ExampleTests.cs': ` using System; @@ -376,7 +376,7 @@ test('should be able to override context options', async ({ runTest }) => { Assert.Equal("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); - var response = await Page.GotoAsync("https://example.com/"); + var response = await Page.GotoAsync("${server.EMPTY_PAGE}"); Assert.Equal("KekStarValue", await response.Request.HeaderValueAsync("Kekstar")); } diff --git a/src/Playwright.TestingHarnessTest/tests/xunit.v3.spec.ts b/src/Playwright.TestingHarnessTest/tests/xunit.v3.spec.ts new file mode 100644 index 0000000000..b278231582 --- /dev/null +++ b/src/Playwright.TestingHarnessTest/tests/xunit.v3.spec.ts @@ -0,0 +1,574 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { test, expect } from './baseTest'; + +test.use({ testMode: 'xunit.v3' }); + +test('should be able to forward DEBUG=pw:api env var', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + try + { + await Page.Locator("button").ClickAsync(new() { Timeout = 1_000 }); + } + catch + { + } + } + }`, + '.runsettings': ` + + + + + pw:api + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + // TODO: change back to .stdout so the stdout is correlated with the test + expect(result.rawStdout).toContain("pw:api") + expect(result.rawStdout).toContain("element is not enabled") + expect(result.rawStdout).toContain("retrying click action") +}); + +test('should be able to set the browser via the runsettings file', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("BrowserType: " + BrowserType.Name); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + webkit + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: webkit") + expect(result.stdout).toContain("BrowserType: webkit") + expect(/User-Agent: .*WebKit.*/.test(result.stdout)).toBeTruthy() +}); + +test('should prioritize browser from env over the runsettings file', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("BrowserType: " + BrowserType.Name); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + webkit + + + `, + }, 'dotnet test --settings=.runsettings', { + BROWSER: 'firefox' + }); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: firefox") + expect(result.stdout).toContain("BrowserType: firefox") + expect(/User-Agent: .*Firefox.*/.test(result.stdout)).toBeTruthy() +}); + +test('should be able to make the browser headed via the env', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + }, 'dotnet test', { + HEADED: '1' + }); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: chromium") + expect(result.stdout).not.toContain("Headless") +}); + +test('should be able to parse BrowserName and LaunchOptions.Headless from runsettings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + false + + + firefox + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: firefox") + expect(result.stdout).not.toContain("Headless") +}); + +test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer, server }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + await Page.GotoAsync("${server.EMPTY_PAGE}"); + } + }`, + '.runsettings': ` + + + + chromium + + false + + ${proxyServer.listenAddr()} + user + pwd + + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + + expect(result.stdout).not.toContain("Headless"); + + const { url, auth } = proxyServer.requests.find(r => r.url === server.EMPTY_PAGE)!; + expect(url).toBe(server.EMPTY_PAGE); + expect(auth).toBe('user:pwd'); +}); + +test('should be able to parse LaunchOptions.Args from runsettings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + ['--user-agent=hello'] + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("User-Agent: hello") +}); + +test('should be able to override context options', async ({ runTest, server }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + + Assert.False(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: light)').matches")); + Assert.True(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: dark)').matches")); + + Assert.Equal(1920, await Page.EvaluateAsync("() => window.innerWidth")); + Assert.Equal(1080, await Page.EvaluateAsync("() => window.innerHeight")); + + Assert.Equal("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); + + var response = await Page.GotoAsync("${server.EMPTY_PAGE}"); + Assert.Equal("KekStarValue", await response.Request.HeaderValueAsync("Kekstar")); + } + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions() + { + ColorScheme = ColorScheme.Dark, + UserAgent = "Foobar", + ViewportSize = new() + { + Width = 1920, + Height = 1080 + }, + ExtraHTTPHeaders = new Dictionary { + { "Kekstar", "KekStarValue" } + } + }; + } + }`}, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); +}); + +test('should be able to override launch options', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + false + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).not.toContain("Headless"); +}); + +test.describe('Expect() timeout', () => { + test('should have 5 seconds by default', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text"); + } + }`, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("Expect \"ToHaveTextAsync\" with timeout 5000ms") + }); + + test('should be able to override it via each Expect() call', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text", new() { Timeout = 100 }); + } + }`, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("Expect \"ToHaveTextAsync\" with timeout 100ms") + }); + + test('should be able to override it via the global config', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text"); + } + }`, + '.runsettings': ` + + + + 123 + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("Expect \"ToHaveTextAsync\" with timeout 123ms") + }); +}); + +test.describe('ConnectOptions', () => { + const ExampleTestWithConnectOptions = ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + 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); + }); +}); diff --git a/src/Playwright.Tests/ChromiumLauncherTests.cs b/src/Playwright.Tests/ChromiumLauncherTests.cs deleted file mode 100644 index 30c59c183c..0000000000 --- a/src/Playwright.Tests/ChromiumLauncherTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -/* - * MIT License - * - * Copyright (c) Microsoft Corporation. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and / or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Microsoft.Playwright.Tests.Firefox; - -public class ChromiumLauncherTests : PlaywrightTestEx -{ - [PlaywrightTest("chromium/launcher.spec.ts", "should return background pages")] - [Skip(SkipAttribute.Targets.Webkit, SkipAttribute.Targets.Firefox)] - public async Task ShouldReturnBackgroundPages() - { - using var userDataDir = new TempDirectory(); - var extensionPath = TestUtils.GetAsset("simple-extension"); - var extensionOptions = new BrowserTypeLaunchPersistentContextOptions - { - Headless = false, - Args = new[] { - $"--disable-extensions-except={extensionPath}", - $"--load-extension={extensionPath}", - }, - }; - var context = await BrowserType.LaunchPersistentContextAsync(userDataDir.Path, extensionOptions); - var backgroundPages = context.BackgroundPages; - var backgroundPage = backgroundPages.Count > 0 - ? backgroundPages[0] - : await WaitForBackgroundPage(context); - Assert.NotNull(backgroundPage); - Assert.Contains(backgroundPage, context.BackgroundPages.ToList()); - Assert.False(context.Pages.Contains(backgroundPage)); - await context.CloseAsync(); - Assert.IsEmpty(context.Pages); - Assert.IsEmpty(context.BackgroundPages); - } - - [PlaywrightTest("chromium/launcher.spec.ts", "should return background pages when recording video")] - [Skip(SkipAttribute.Targets.Webkit, SkipAttribute.Targets.Firefox)] - public async Task ShouldReturnBackgroundPagesWhenRecordingVideo() - { - using var tempDirectory = new TempDirectory(); - using var userDataDir = new TempDirectory(); - var extensionPath = TestUtils.GetAsset("simple-extension"); - var extensionOptions = new BrowserTypeLaunchPersistentContextOptions - { - Headless = false, - Args = new[] { - $"--disable-extensions-except={extensionPath}", - $"--load-extension={extensionPath}", - }, - RecordVideoDir = tempDirectory.Path, - }; - var context = await BrowserType.LaunchPersistentContextAsync(userDataDir.Path, extensionOptions); - var backgroundPages = context.BackgroundPages; - - var backgroundPage = backgroundPages.Count > 0 - ? backgroundPages[0] - : await WaitForBackgroundPage(context); - Assert.NotNull(backgroundPage); - Assert.Contains(backgroundPage, context.BackgroundPages.ToList()); - Assert.False(context.Pages.Contains(backgroundPage)); - await context.CloseAsync(); - } - - private async Task WaitForBackgroundPage(IBrowserContext context) - { - var tsc = new TaskCompletionSource(); - context.BackgroundPage += (_, e) => tsc.TrySetResult(e); - return await tsc.Task; - } -} diff --git a/src/Playwright.Tests/InterceptionTests.cs b/src/Playwright.Tests/InterceptionTests.cs index 8bd629f16a..14cb129bf7 100644 --- a/src/Playwright.Tests/InterceptionTests.cs +++ b/src/Playwright.Tests/InterceptionTests.cs @@ -88,6 +88,13 @@ bool URLMatches(string baseURL, string url, string glob) Assert.True(URLMatches("http://playwright.dev", "http://playwright.dev/?x=y", "?x=y")); Assert.True(URLMatches("http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y")); + // Case insensitive matching + Assert.True(URLMatches(null, "https://playwright.dev/fooBAR", "HtTpS://pLaYwRiGhT.dEv/fooBAR")); + Assert.True(URLMatches("http://ignored", "https://playwright.dev/fooBAR", "HtTpS://pLaYwRiGhT.dEv/fooBAR")); + // Path and search query are case-sensitive + Assert.False(URLMatches(null, "https://playwright.dev/foobar", "https://playwright.dev/fooBAR")); + Assert.False(URLMatches(null, "https://playwright.dev/foobar?a=b", "https://playwright.dev/foobar?A=B")); + // This is not supported, we treat ? as a query separator. Assert.That("http://localhost:8080/Simple/path.js", Does.Not.Match(GlobToRegex("http://localhost:8080/?imple/path.js"))); Assert.False(URLMatches(null, "http://playwright.dev/", "http://playwright.?ev")); diff --git a/src/Playwright.Tests/Playwright.Tests.csproj b/src/Playwright.Tests/Playwright.Tests.csproj index e606aa57fb..e07f8ca366 100644 --- a/src/Playwright.Tests/Playwright.Tests.csproj +++ b/src/Playwright.Tests/Playwright.Tests.csproj @@ -29,7 +29,7 @@ - +
diff --git a/src/Playwright.Tests/assets/simple-extension/content-script.js b/src/Playwright.Tests/assets/simple-extension/content-script.js deleted file mode 100644 index 965f99fd3d..0000000000 --- a/src/Playwright.Tests/assets/simple-extension/content-script.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log('hey from the content-script'); -self.thisIsTheContentScript = true; - diff --git a/src/Playwright.Tests/assets/simple-extension/index.js b/src/Playwright.Tests/assets/simple-extension/index.js deleted file mode 100644 index a0bb3f4eae..0000000000 --- a/src/Playwright.Tests/assets/simple-extension/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// Mock script for background extension -window.MAGIC = 42; diff --git a/src/Playwright.Tests/assets/simple-extension/manifest.json b/src/Playwright.Tests/assets/simple-extension/manifest.json deleted file mode 100644 index da2cd082ed..0000000000 --- a/src/Playwright.Tests/assets/simple-extension/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Simple extension", - "version": "0.1", - "background": { - "scripts": ["index.js"] - }, - "content_scripts": [{ - "matches": [""], - "css": [], - "js": ["content-script.js"] - }], - "permissions": ["background", "activeTab"], - "manifest_version": 2 -} diff --git a/src/Playwright.Xunit.v3/BrowserService.cs b/src/Playwright.Xunit.v3/BrowserService.cs new file mode 100644 index 0000000000..53be2802e3 --- /dev/null +++ b/src/Playwright.Xunit.v3/BrowserService.cs @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +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; +using Microsoft.Playwright.TestAdapter; + +namespace Microsoft.Playwright.Xunit.v3; + +internal class BrowserService : IWorkerService +{ + public IBrowser Browser { get; private set; } + + private BrowserService(IBrowser browser) + { + Browser = browser; + } + + public static Task Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions) + { + return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false))); + } + + private static async Task 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 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 null; + } + + var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? ""; + 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 + { + Timeout = 3 * 60 * 1000, + ExposeNetwork = exposeNetwork, + Headers = new Dictionary + { + ["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(); +} diff --git a/src/Playwright.Xunit.v3/BrowserTest.cs b/src/Playwright.Xunit.v3/BrowserTest.cs new file mode 100644 index 0000000000..1529f8ce6e --- /dev/null +++ b/src/Playwright.Xunit.v3/BrowserTest.cs @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Xunit.v3; + +public class BrowserTest : PlaywrightTest +{ + public IBrowser Browser { get; internal set; } = null!; + private readonly List _contexts = new(); + + public async Task NewContext(BrowserNewContextOptions? options = null) + { + var context = await Browser.NewContextAsync(options).ConfigureAwait(false); + _contexts.Add(context); + return context; + } + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false); + Browser = service.Browser; + } + + public override async ValueTask DisposeAsync() + { + if (TestOk()) + { + foreach (var context in _contexts) + { + await context.CloseAsync().ConfigureAwait(false); + } + } + _contexts.Clear(); + Browser = null!; + await base.DisposeAsync().ConfigureAwait(false); + } + + public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null); +} diff --git a/src/Playwright.Xunit.v3/ContextTest.cs b/src/Playwright.Xunit.v3/ContextTest.cs new file mode 100644 index 0000000000..76622275e0 --- /dev/null +++ b/src/Playwright.Xunit.v3/ContextTest.cs @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Xunit.v3; + +public class ContextTest : BrowserTest +{ + public IBrowserContext Context { get; private set; } = null!; + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + Context = await NewContext(ContextOptions()).ConfigureAwait(false); + } + + public virtual BrowserNewContextOptions ContextOptions() + { + return new() + { + Locale = "en-US", + ColorScheme = ColorScheme.Light, + }; + } +} diff --git a/src/Playwright.Xunit.v3/PageTest.cs b/src/Playwright.Xunit.v3/PageTest.cs new file mode 100644 index 0000000000..abe0df2c7d --- /dev/null +++ b/src/Playwright.Xunit.v3/PageTest.cs @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Xunit.v3; + +public class PageTest : ContextTest +{ + public IPage Page { get; private set; } = null!; + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + Page = await Context.NewPageAsync().ConfigureAwait(false); + } +} diff --git a/src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj b/src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj new file mode 100644 index 0000000000..eb293814da --- /dev/null +++ b/src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj @@ -0,0 +1,45 @@ + + + + Microsoft.Playwright.Xunit.v3 + Microsoft.Playwright.Xunit.v3 + A set of helpers and fixtures to enable using Playwright in xUnit tests. + + Playwright enables reliable end-to-end testing for modern web apps. This package brings in additional helpers + and fixtures to enable using it within xUnit. + + icon.png + netstandard2.0 + true + true + Microsoft.Playwright.Xunit.v3 + 0.0.0 + True + Microsoft.Playwright.Xunit.v3 + ./nupkg + true + enable + README.md + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/Playwright.Xunit.v3/PlaywrightTest.cs b/src/Playwright.Xunit.v3/PlaywrightTest.cs new file mode 100644 index 0000000000..0c99831ca4 --- /dev/null +++ b/src/Playwright.Xunit.v3/PlaywrightTest.cs @@ -0,0 +1,55 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Threading.Tasks; +using Microsoft.Playwright.TestAdapter; + +namespace Microsoft.Playwright.Xunit.v3; + +public class PlaywrightTest : WorkerAwareTest +{ + public string BrowserName { get; internal set; } = null!; + + private static readonly Task _playwrightTask = Microsoft.Playwright.Playwright.CreateAsync(); + + public IPlaywright Playwright { get; private set; } = null!; + public IBrowserType BrowserType { get; private set; } = null!; + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + Playwright = await _playwrightTask.ConfigureAwait(false); + BrowserName = PlaywrightSettingsProvider.BrowserName; + BrowserType = Playwright[BrowserName]; + Playwright.Selectors.SetTestIdAttribute("data-testid"); + } + + public static void SetDefaultExpectTimeout(float timeout) => Assertions.SetDefaultExpectTimeout(timeout); + + public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); + + public IPageAssertions Expect(IPage page) => Assertions.Expect(page); + + public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); +} diff --git a/src/Playwright.Xunit.v3/WorkerAwareTest.cs b/src/Playwright.Xunit.v3/WorkerAwareTest.cs new file mode 100644 index 0000000000..e07b625399 --- /dev/null +++ b/src/Playwright.Xunit.v3/WorkerAwareTest.cs @@ -0,0 +1,116 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Playwright.Core; +using Microsoft.Playwright.TestAdapter; +using Xunit; + +namespace Microsoft.Playwright.Xunit.v3; + +public class WorkerAwareTest : IAsyncLifetime +{ + private static readonly ConcurrentStack _allWorkers = new(); + private Worker _currentWorker = null!; + + public WorkerAwareTest() + { + PlaywrightSettingsProvider.LoadViaEnvIfNeeded(); + } + + internal class Worker + { + private static int _lastWorkedIndex = 0; + public int WorkerIndex = Interlocked.Increment(ref _lastWorkedIndex); + public Dictionary Services = []; + } + + public int WorkerIndex { get; internal set; } + + public async Task RegisterService(string name, Func> factory) where T : class, IWorkerService + { + if (!_currentWorker.Services.ContainsKey(name)) + { + _currentWorker.Services[name] = await factory().ConfigureAwait(false); + } + + return (_currentWorker.Services[name] as T)!; + } + + public virtual ValueTask InitializeAsync() + { + if (!_allWorkers.TryPop(out _currentWorker!)) + { + _currentWorker = new(); + } + WorkerIndex = _currentWorker.WorkerIndex; + if (PlaywrightSettingsProvider.ExpectTimeout.HasValue) + { + AssertionsBase.SetDefaultTimeout(PlaywrightSettingsProvider.ExpectTimeout.Value); + } + return new ValueTask(); + } + + public async virtual ValueTask DisposeAsync() + { + if (TestOk()) + { + foreach (var kv in _currentWorker.Services) + { + await kv.Value.ResetAsync().ConfigureAwait(false); + } + _allWorkers.Push(_currentWorker); + } + else + { + foreach (var kv in _currentWorker.Services) + { + await kv.Value.DisposeAsync().ConfigureAwait(false); + } + _currentWorker.Services.Clear(); + } + } + + protected bool TestOk() + { + // Test is still running. + if (TestContext.Current.TestState == null) + { + return false; + } + return + TestContext.Current.TestState.Result == TestResult.Passed || + TestContext.Current.TestState.Result == TestResult.Skipped; + } +} + +public interface IWorkerService +{ + public Task ResetAsync(); + public Task DisposeAsync(); +} diff --git a/src/Playwright.Xunit/Playwright.Xunit.csproj b/src/Playwright.Xunit/Playwright.Xunit.csproj index a2896fe895..4dcf4f239e 100644 --- a/src/Playwright.Xunit/Playwright.Xunit.csproj +++ b/src/Playwright.Xunit/Playwright.Xunit.csproj @@ -19,6 +19,7 @@ ./nupkg true enable + README.md true 1.52.0 @@ -41,5 +42,6 @@ + diff --git a/src/Playwright.sln b/src/Playwright.sln index a8da083c63..2c6dabd1c9 100644 --- a/src/Playwright.sln +++ b/src/Playwright.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31516.353 MinimumVisualStudioVersion = 10.0.40219.1 @@ -34,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.TestingHarnessTe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.Xunit", "Playwright.Xunit\Playwright.Xunit.csproj", "{7E427229-793C-44E5-B90E-FB8E322066FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.Xunit.v3", "Playwright.Xunit.v3\Playwright.Xunit.v3.csproj", "{6CFF37A8-7C6B-4203-9BF7-3A83752653E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.MSTest.v4", "Playwright.MSTest.v4\Playwright.MSTest.v4.csproj", "{391F8C08-8C76-4D26-BBCF-8A3E00252075}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +85,14 @@ Global {7E427229-793C-44E5-B90E-FB8E322066FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E427229-793C-44E5-B90E-FB8E322066FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E427229-793C-44E5-B90E-FB8E322066FA}.Release|Any CPU.Build.0 = Release|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Release|Any CPU.Build.0 = Release|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Debug|Any CPU.Build.0 = Debug|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Release|Any CPU.ActiveCfg = Release|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Playwright/API/Generated/IBrowserType.cs b/src/Playwright/API/Generated/IBrowserType.cs index 995fe9feff..54164dfe4e 100644 --- a/src/Playwright/API/Generated/IBrowserType.cs +++ b/src/Playwright/API/Generated/IBrowserType.cs @@ -174,6 +174,12 @@ public partial interface IBrowserType /// seen at chrome://version. /// Note that browsers do not allow launching multiple instances with the same User /// Data Directory. + /// Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome + /// user profile is not supported. Pointing userDataDir to Chrome's main "User + /// Data" directory (the profile used for your regular browsing) may result in pages + /// not loading or the browser exiting. Create and use a separate directory (for example, + /// an empty folder) as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port + /// for details. /// /// Call options Task LaunchPersistentContextAsync(string userDataDir, BrowserTypeLaunchPersistentContextOptions? options = default); diff --git a/src/Playwright/API/Generated/IPage.cs b/src/Playwright/API/Generated/IPage.cs index e4e19664c7..ea9ae6fbc5 100644 --- a/src/Playwright/API/Generated/IPage.cs +++ b/src/Playwright/API/Generated/IPage.cs @@ -1582,7 +1582,7 @@ public partial interface IPage /// /// /// Pauses script execution. Playwright will stop executing the script and wait for - /// the user to either press 'Resume' button in the page overlay or to call playwright.resume() + /// the user to either press the 'Resume' button in the page overlay or to call playwright.resume() /// in the DevTools console. /// /// diff --git a/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs b/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs index c4ff393442..71068b6e15 100644 --- a/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs +++ b/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs @@ -91,6 +91,12 @@ public APIRequestNewContextOptions(APIRequestNewContextOptions clone) /// that the certificate is valid for. /// /// + /// Client certificate authentication is only active when at least one client certificate + /// is provided. If you want to reject all client certificates sent by the server, you + /// need to provide a client certificate with an origin that does not match any + /// of the domains you plan to visit. + /// + /// /// When using WebKit on macOS, accessing localhost will not pick up client certificates. /// You can make it work by replacing localhost with local.playwright. /// diff --git a/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs b/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs index 5a671f9297..da6d9e9fe8 100644 --- a/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserNewContextOptions.cs @@ -131,6 +131,12 @@ public BrowserNewContextOptions(BrowserNewContextOptions clone) /// that the certificate is valid for. /// /// + /// Client certificate authentication is only active when at least one client certificate + /// is provided. If you want to reject all client certificates sent by the server, you + /// need to provide a client certificate with an origin that does not match any + /// of the domains you plan to visit. + /// + /// /// When using WebKit on macOS, accessing localhost will not pick up client certificates. /// You can make it work by replacing localhost with local.playwright. /// diff --git a/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs b/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs index be99eb6f5e..71f9550be2 100644 --- a/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserNewPageOptions.cs @@ -131,6 +131,12 @@ public BrowserNewPageOptions(BrowserNewPageOptions clone) /// that the certificate is valid for. /// /// + /// Client certificate authentication is only active when at least one client certificate + /// is provided. If you want to reject all client certificates sent by the server, you + /// need to provide a client certificate with an origin that does not match any + /// of the domains you plan to visit. + /// + /// /// When using WebKit on macOS, accessing localhost will not pick up client certificates. /// You can make it work by replacing localhost with local.playwright. /// diff --git a/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs b/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs index 1b05b92571..83ff8f0828 100644 --- a/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs @@ -181,6 +181,12 @@ public BrowserTypeLaunchPersistentContextOptions(BrowserTypeLaunchPersistentCont /// that the certificate is valid for. /// /// + /// Client certificate authentication is only active when at least one client certificate + /// is provided. If you want to reject all client certificates sent by the server, you + /// need to provide a client certificate with an origin that does not match any + /// of the domains you plan to visit. + /// + /// /// When using WebKit on macOS, accessing localhost will not pick up client certificates. /// You can make it work by replacing localhost with local.playwright. /// diff --git a/src/Playwright/Core/AssertionsBase.cs b/src/Playwright/Core/AssertionsBase.cs index fdb6bea588..22ed2b791b 100644 --- a/src/Playwright/Core/AssertionsBase.cs +++ b/src/Playwright/Core/AssertionsBase.cs @@ -33,8 +33,10 @@ using Microsoft.Playwright.Transport.Protocol; [assembly: InternalsVisibleTo("Microsoft.Playwright.MSTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] +[assembly: InternalsVisibleTo("Microsoft.Playwright.MSTest.v4, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.NUnit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.Xunit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] +[assembly: InternalsVisibleTo("Microsoft.Playwright.Xunit.v3, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] namespace Microsoft.Playwright.Core; diff --git a/src/Playwright/Core/Page.cs b/src/Playwright/Core/Page.cs index 89a853688e..7c723a0b5a 100644 --- a/src/Playwright/Core/Page.cs +++ b/src/Playwright/Core/Page.cs @@ -1148,10 +1148,10 @@ public Task IsVisibleAsync(string selector, PageIsVisibleOptions? options [MethodImpl(MethodImplOptions.NoInlining)] public async Task PauseAsync() { - var defaultNavigationTimeout = _timeoutSettings.DefaultNavigationTimeout; - var defaultTimeout = _timeoutSettings.DefaultTimeout; - _timeoutSettings.SetDefaultNavigationTimeout(0); - _timeoutSettings.SetDefaultTimeout(0); + var defaultNavigationTimeout = Context._timeoutSettings.DefaultNavigationTimeout; + var defaultTimeout = Context._timeoutSettings.DefaultTimeout; + Context.SetDefaultNavigationTimeout(0); + Context.SetDefaultTimeout(0); try { await Task.WhenAny(Context.SendMessageToServerAsync("pause"), ClosedOrCrashedTcs.Task).ConfigureAwait(false); diff --git a/src/Playwright/Core/Selectors.cs b/src/Playwright/Core/Selectors.cs index 506fd7c9ca..44c627d2c0 100644 --- a/src/Playwright/Core/Selectors.cs +++ b/src/Playwright/Core/Selectors.cs @@ -53,7 +53,7 @@ public async Task RegisterAsync(string name, SelectorsRegisterOptions? options = { engine["contentScript"] = options.ContentScript; } - foreach (var context in _contextsForSelectors) + foreach (var context in _contextsForSelectors.ToArray()) { await context.SendMessageToServerAsync("registerSelectorEngine", new Dictionary { @@ -67,7 +67,7 @@ public void SetTestIdAttribute(string attributeName) { Locator.SetTestIdAttribute(attributeName); _testIdAttributeName = attributeName; - foreach (var context in _contextsForSelectors) + foreach (var context in _contextsForSelectors.ToArray()) { context.SendMessageToServerAsync( "setTestIdAttributeName", diff --git a/src/Playwright/Helpers/URLMatch.cs b/src/Playwright/Helpers/URLMatch.cs index 86f8c4dfe0..a2b07b6b46 100644 --- a/src/Playwright/Helpers/URLMatch.cs +++ b/src/Playwright/Helpers/URLMatch.cs @@ -90,11 +90,7 @@ internal static string ConstructURLBasedOnBaseURL(string? baseUrl, string url) { try { - if (string.IsNullOrEmpty(baseUrl)) - { - return FixupTrailingSlash(new Uri(url, UriKind.Absolute)).ToString(); - } - return FixupTrailingSlash(new Uri(new Uri(baseUrl), new Uri(url, UriKind.RelativeOrAbsolute))).ToString(); + return ResolveBaseURL(baseUrl, url).Resolved; } catch { @@ -254,16 +250,45 @@ string MapToken(string original, string replacement) return newPrefix + newSuffix; })); - var resolved = ConstructURLBasedOnBaseURL(baseURL, relativePath); + var (resolved, caseInsensitivePart) = ResolveBaseURL(baseURL, relativePath); foreach (var kvp in tokenMap) { - resolved = resolved.Replace(kvp.Key, kvp.Value); + var normalize = caseInsensitivePart?.Contains(kvp.Key) == true; + resolved = resolved.Replace(kvp.Key, normalize ? kvp.Value.ToLowerInvariant() : kvp.Value); } match = resolved; } return match; } + private static (string Resolved, string? CaseInsensitivePart) ResolveBaseURL(string? baseUrl, string url) + { + try + { + Uri uri; + if (string.IsNullOrEmpty(baseUrl)) + { + uri = FixupTrailingSlash(new Uri(url, UriKind.Absolute)); + } + else + { + uri = FixupTrailingSlash(new Uri(new Uri(baseUrl), new Uri(url, UriKind.RelativeOrAbsolute))); + } + var resolved = uri.ToString(); + // Schema and domain are case-insensitive. + var caseInsensitivePrefix = $"{uri.Scheme}://{uri.Host}"; + if (!uri.IsDefaultPort) + { + caseInsensitivePrefix += $":{uri.Port}"; + } + return (resolved, caseInsensitivePrefix); + } + catch + { + return (url, null); + } + } + public bool Equals(string? globMatch, Regex? reMatch, Func? funcMatch, string? baseURL, bool isWebSocketUrl) { return this.re?.ToString() == reMatch?.ToString() && this.re?.Options == reMatch?.Options diff --git a/src/Playwright/Playwright.csproj b/src/Playwright/Playwright.csproj index 2953b25013..45dbd041b7 100644 --- a/src/Playwright/Playwright.csproj +++ b/src/Playwright/Playwright.csproj @@ -23,6 +23,7 @@ true 1.52.0 enable + README.md @@ -54,6 +55,7 @@ + diff --git a/src/Playwright/Transport/Protocol/Generated/Point.cs b/src/Playwright/Transport/Protocol/Generated/Point.cs index ea299e7093..2366cddfc8 100644 --- a/src/Playwright/Transport/Protocol/Generated/Point.cs +++ b/src/Playwright/Transport/Protocol/Generated/Point.cs @@ -29,8 +29,8 @@ namespace Microsoft.Playwright.Transport.Protocol; internal class Point { [JsonPropertyName("x")] - public int X { get; set; } + public float X { get; set; } [JsonPropertyName("y")] - public int Y { get; set; } + public float Y { get; set; } } diff --git a/src/Playwright/Transport/Protocol/Generated/Rect.cs b/src/Playwright/Transport/Protocol/Generated/Rect.cs index 22069d2178..9c618dee21 100644 --- a/src/Playwright/Transport/Protocol/Generated/Rect.cs +++ b/src/Playwright/Transport/Protocol/Generated/Rect.cs @@ -29,14 +29,14 @@ namespace Microsoft.Playwright.Transport.Protocol; internal class Rect { [JsonPropertyName("x")] - public int X { get; set; } + public float X { get; set; } [JsonPropertyName("y")] - public int Y { get; set; } + public float Y { get; set; } [JsonPropertyName("width")] - public int Width { get; set; } + public float Width { get; set; } [JsonPropertyName("height")] - public int Height { get; set; } + public float Height { get; set; } } diff --git a/src/Playwright/Transport/Protocol/Generated/ResourceTiming.cs b/src/Playwright/Transport/Protocol/Generated/ResourceTiming.cs index ef23b8065d..228ce39e64 100644 --- a/src/Playwright/Transport/Protocol/Generated/ResourceTiming.cs +++ b/src/Playwright/Transport/Protocol/Generated/ResourceTiming.cs @@ -29,26 +29,26 @@ namespace Microsoft.Playwright.Transport.Protocol; internal class ResourceTiming { [JsonPropertyName("startTime")] - public int StartTime { get; set; } + public float StartTime { get; set; } [JsonPropertyName("domainLookupStart")] - public int DomainLookupStart { get; set; } + public float DomainLookupStart { get; set; } [JsonPropertyName("domainLookupEnd")] - public int DomainLookupEnd { get; set; } + public float DomainLookupEnd { get; set; } [JsonPropertyName("connectStart")] - public int ConnectStart { get; set; } + public float ConnectStart { get; set; } [JsonPropertyName("secureConnectionStart")] - public int SecureConnectionStart { get; set; } + public float SecureConnectionStart { get; set; } [JsonPropertyName("connectEnd")] - public int ConnectEnd { get; set; } + public float ConnectEnd { get; set; } [JsonPropertyName("requestStart")] - public int RequestStart { get; set; } + public float RequestStart { get; set; } [JsonPropertyName("responseStart")] - public int ResponseStart { get; set; } + public float ResponseStart { get; set; } } diff --git a/src/Playwright/Transport/Protocol/Generated/SecurityDetails.cs b/src/Playwright/Transport/Protocol/Generated/SecurityDetails.cs index 67b115c1a7..c42a293f69 100644 --- a/src/Playwright/Transport/Protocol/Generated/SecurityDetails.cs +++ b/src/Playwright/Transport/Protocol/Generated/SecurityDetails.cs @@ -38,8 +38,8 @@ internal class SecurityDetails public string SubjectName { get; set; } = null!; [JsonPropertyName("validFrom")] - public int? ValidFrom { get; set; } + public float ValidFrom { get; set; } [JsonPropertyName("validTo")] - public int? ValidTo { get; set; } + public float ValidTo { get; set; } } diff --git a/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs b/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs index 65c9c3c1bb..a54cb1732f 100644 --- a/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs +++ b/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs @@ -30,7 +30,7 @@ namespace Microsoft.Playwright.Transport.Protocol; internal class SerializedValue { [JsonPropertyName("n")] - public int? N { get; set; } + public float N { get; set; } [JsonPropertyName("b")] public bool? B { get; set; }