diff --git a/.github/workflows/functional_all_db.yml b/.github/workflows/functional_all_db.yml index aa43eeca2c1..8b74fb306ff 100644 --- a/.github/workflows/functional_all_db.yml +++ b/.github/workflows/functional_all_db.yml @@ -33,31 +33,23 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-dotnet - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "20" - package-manager-cache: false - - name: Cache Playwright Browsers - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} + - name: Build + run: | + dotnet build -c Release test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj - name: Install Playwright Browsers run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests + env: + ORCHARD_APP: mvc run: | - yarn workspace @orchardcore/tests-playwright test:mvc + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Mvc*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: functional-mvc-failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ + src/OrchardCore.Mvc.Web/App_Data_Tests/logs retention-days: 3 test_functional_cms_sqlite: @@ -72,31 +64,20 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-dotnet - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "20" - package-manager-cache: false - - name: Cache Playwright Browsers - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} + - name: Build + run: | + dotnet build -c Release test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj - name: Install Playwright Browsers run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: functional-cms-sqlite-failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs retention-days: 3 @@ -129,31 +110,20 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-dotnet - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "20" - package-manager-cache: false - - name: Cache Playwright Browsers - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} + - name: Build + run: | + dotnet build -c Release test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj - name: Install Playwright Browsers run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: functional-cms-postgresql-failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs retention-days: 3 @@ -180,31 +150,20 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-dotnet - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "20" - package-manager-cache: false - - name: Cache Playwright Browsers - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} + - name: Build + run: | + dotnet build -c Release test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj - name: Install Playwright Browsers run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: functional-cms-mysql-failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs retention-days: 3 @@ -230,30 +189,19 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/setup-dotnet - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - with: - node-version: "20" - package-manager-cache: false - - name: Cache Playwright Browsers - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} + - name: Build + run: | + dotnet build -c Release test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj - name: Install Playwright Browsers run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: functional-cms-mssql-failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs retention-days: 3 diff --git a/.github/workflows/main_ci.yml b/.github/workflows/main_ci.yml index 51db33d3184..c92934195d1 100644 --- a/.github/workflows/main_ci.yml +++ b/.github/workflows/main_ci.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: "20" + node-version: "15" package-manager-cache: false - uses: ./.github/actions/setup-dotnet - name: Build @@ -36,32 +36,23 @@ jobs: - name: Unit Tests run: | dotnet test --project ./test/OrchardCore.Tests/OrchardCore.Tests.csproj -c Release --no-build - - name: Cache Playwright Browsers - if: matrix.os == 'ubuntu-24.04' - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} - name: Install Playwright Browsers if: matrix.os == 'ubuntu-24.04' run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests - CMS if: matrix.os == 'ubuntu-24.04' run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - name: Functional Tests - MVC if: matrix.os == 'ubuntu-24.04' + env: + ORCHARD_APP: mvc run: | - yarn workspace @orchardcore/tests-playwright test:mvc + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Mvc*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: matrix.os == 'ubuntu-24.04' && failure() with: name: Functional Test failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml index d4f1d25bb60..5fd8b69f520 100644 --- a/.github/workflows/pr_ci.yml +++ b/.github/workflows/pr_ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: "20" + node-version: "15" package-manager-cache: false - uses: ./.github/actions/setup-dotnet - name: Build @@ -33,33 +33,23 @@ jobs: - name: Unit Tests run: | dotnet test --project ./test/OrchardCore.Tests/OrchardCore.Tests.csproj -c Release --no-build - - name: Cache Playwright Browsers - if: matrix.os == 'ubuntu-24.04' - id: playwright-cache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} - name: Install Playwright Browsers if: matrix.os == 'ubuntu-24.04' run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests - CMS if: matrix.os == 'ubuntu-24.04' run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - name: Functional Tests - MVC if: matrix.os == 'ubuntu-24.04' + env: + ORCHARD_APP: mvc run: | - yarn workspace @orchardcore/tests-playwright test:mvc + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Mvc*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: matrix.os == 'ubuntu-24.04' && failure() with: name: functional-test-failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs diff --git a/.github/workflows/preview_ci.yml b/.github/workflows/preview_ci.yml index 24c57337e19..ca20f38f11d 100644 --- a/.github/workflows/preview_ci.yml +++ b/.github/workflows/preview_ci.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 if: steps.check-publish.outputs.should-publish == 'true' with: - node-version: "20" + node-version: "15" package-manager-cache: false - uses: ./.github/actions/setup-dotnet if: steps.check-publish.outputs.should-publish == 'true' @@ -42,34 +42,25 @@ jobs: if: steps.check-publish.outputs.should-publish == 'true' run: | dotnet test --project ./test/OrchardCore.Tests/OrchardCore.Tests.csproj -c Release --no-build - - name: Cache Playwright Browsers - if: steps.check-publish.outputs.should-publish == 'true' - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} - name: Install Playwright Browsers if: steps.check-publish.outputs.should-publish == 'true' run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests - CMS if: steps.check-publish.outputs.should-publish == 'true' run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - name: Functional Tests - MVC if: steps.check-publish.outputs.should-publish == 'true' + env: + ORCHARD_APP: mvc run: | - yarn workspace @orchardcore/tests-playwright test:mvc + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Mvc*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: failure() with: name: Functional Test failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs - name: Deploy preview NuGet packages if: steps.check-publish.outputs.should-publish == 'true' diff --git a/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml index f25483665f7..57004597417 100644 --- a/.github/workflows/release_ci.yml +++ b/.github/workflows/release_ci.yml @@ -30,7 +30,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: "20" + node-version: "15" package-manager-cache: false - uses: ./.github/actions/setup-dotnet - name: Set build number @@ -44,34 +44,25 @@ jobs: - name: Unit Tests run: | dotnet test --project ./test/OrchardCore.Tests/OrchardCore.Tests.csproj -c Release --no-build - - name: Cache Playwright Browsers - if: matrix.os == 'ubuntu-24.04' - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ hashFiles('yarn.lock') }} - name: Install Playwright Browsers if: matrix.os == 'ubuntu-24.04' run: | - npm install -g corepack - corepack enable - yarn install - yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium + pwsh test/OrchardCore.Tests.Functional/bin/Release/net10.0/playwright.ps1 install --with-deps chromium - name: Functional Tests - CMS if: matrix.os == 'ubuntu-24.04' run: | - yarn workspace @orchardcore/tests-playwright test:cms + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Cms*" - name: Functional Tests - MVC if: matrix.os == 'ubuntu-24.04' + env: + ORCHARD_APP: mvc run: | - yarn workspace @orchardcore/tests-playwright test:mvc + dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj -c Release --no-build --filter-class "*Mvc*" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 if: matrix.os == 'ubuntu-24.04' && failure() with: name: Functional Test failure path: | - test/OrchardCore.Tests.Playwright/playwright-report/ - test/OrchardCore.Tests.Playwright/test-results/ src/OrchardCore.Cms.Web/App_Data_Tests/logs - name: Deploy release NuGet packages if: matrix.os == 'ubuntu-24.04' diff --git a/.gitignore b/.gitignore index 613e2b23efd..f81025806e1 100644 --- a/.gitignore +++ b/.gitignore @@ -224,5 +224,3 @@ src/Templates/**/content/**/.template.config/ BenchmarkDotNet.Artifacts .playwright-cli -test/OrchardCore.Tests.Playwright/test-results -test/OrchardCore.Tests.Playwright/playwright-report diff --git a/AGENTS.md b/AGENTS.md index 0dcbcecbb8c..5bd3cb187fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,11 +98,14 @@ of each filter. ### Functional Tests (Playwright) -End-to-end tests are located in `test/OrchardCore.Tests.Playwright/`. +End-to-end tests are located in `test/OrchardCore.Tests.Functional/`. ```bash -yarn workspace @orchardcore/tests-playwright test:cms -yarn workspace @orchardcore/tests-playwright test:mvc +# Run CMS functional tests +dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj --filter-class "*Cms*" + +# Run MVC functional tests +ORCHARD_APP=mvc dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj --filter-class "*Mvc*" ``` ### Automated Browser Testing (Playwright MCP) @@ -127,7 +130,7 @@ For AI agents, the Playwright MCP (Model Context Protocol) provides automated br - `test/OrchardCore.Tests/` - Main unit test project - `test/OrchardCore.Abstractions.Tests/` - Tests for abstractions -- `test/OrchardCore.Tests.Playwright/` - Playwright E2E tests +- `test/OrchardCore.Tests.Functional/` - Playwright E2E functional tests - `test/OrchardCore.Tests.Modules/` - Test modules used by tests ## Project Structure @@ -374,16 +377,16 @@ Stop-Process -Id (Get-Content .orchardcore-pid) -Force; Remove-Item .orchardcore ### Functional Testing with Playwright -Create new functional tests under `test/OrchardCore.Tests.Playwright/tests/` following the existing spec patterns. +Create new functional tests under `test/OrchardCore.Tests.Functional/Tests/` following the existing C# test class patterns. Run the Playwright functional tests: ```bash # Run CMS functional tests -yarn workspace @orchardcore/tests-playwright test:cms +dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj --filter-class "*Cms*" # Run MVC functional tests -yarn workspace @orchardcore/tests-playwright test:mvc +ORCHARD_APP=mvc dotnet test test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj --filter-class "*Mvc*" ``` ## Common Extension Points diff --git a/Directory.Packages.props b/Directory.Packages.props index cdec27d62c2..16ba8066db4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -95,6 +95,7 @@ + diff --git a/OrchardCore.slnx b/OrchardCore.slnx index 468906cbcd2..c50e9d0a8ed 100644 --- a/OrchardCore.slnx +++ b/OrchardCore.slnx @@ -288,7 +288,7 @@ - + diff --git a/package.json b/package.json index 3ec8c29f570..c3aed83fac5 100644 --- a/package.json +++ b/package.json @@ -11,22 +11,14 @@ "dry-run": "assets-manager dry-run", "clean": "assets-manager clean", "lint": "eslint .", - "check": "vue-tsc --noEmit", - "test": "yarn workspace @orchardcore/tests-playwright test", - "test:cms": "yarn workspace @orchardcore/tests-playwright test:cms", - "test:mvc": "yarn workspace @orchardcore/tests-playwright test:mvc", - "test:ui": "yarn workspace @orchardcore/tests-playwright test:ui", - "test:headed": "yarn workspace @orchardcore/tests-playwright test:headed", - "test:debug": "yarn workspace @orchardcore/tests-playwright test:debug", - "test:install": "yarn workspace @orchardcore/tests-playwright exec playwright install --with-deps chromium" + "check": "vue-tsc --noEmit" }, "workspaces": [ ".scripts/assets-manager", ".scripts/bloom", "src/Frontend/", "src/OrchardCore.Modules/*/Assets/**", - "src/OrchardCore.Themes/*/Assets/**", - "test/OrchardCore.Tests.Playwright" + "src/OrchardCore.Themes/*/Assets/**" ], "devDependencies": { "@babel/core": "^7.22.11", diff --git a/test/OrchardCore.Tests.Playwright/fixtures/migrations.recipe.json b/test/OrchardCore.Tests.Functional/Fixtures/migrations.recipe.json similarity index 100% rename from test/OrchardCore.Tests.Playwright/fixtures/migrations.recipe.json rename to test/OrchardCore.Tests.Functional/Fixtures/migrations.recipe.json diff --git a/test/OrchardCore.Tests.Functional/Helpers/AppLifecycleHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/AppLifecycleHelper.cs new file mode 100644 index 00000000000..0b9bd16f6ab --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/AppLifecycleHelper.cs @@ -0,0 +1,186 @@ +using System.Diagnostics; +using System.Reflection; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class AppLifecycleHelper +{ + private const string _dotnetVersion = "net10.0"; + + public static void BuildApp(string appDir) + { + Log("Building application..."); + + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"build -c Release -f {_dotnetVersion}", + WorkingDirectory = appDir, + UseShellExecute = false, + }); + + process?.WaitForExit(); + + if (process?.ExitCode != 0) + { + throw new InvalidOperationException($"dotnet build failed with exit code {process?.ExitCode}."); + } + + Log("Build complete."); + } + + public static void DeleteAppData(string appDir, string dataDir = "App_Data_Tests") + { + var fullPath = Path.Combine(appDir, dataDir); + if (Directory.Exists(fullPath)) + { + Directory.Delete(fullPath, recursive: true); + Log($"{fullPath} deleted"); + } + } + + public static void CopyMigrationsRecipe(string appDir) + { + var recipeFileName = "migrations.recipe.json"; + var destDir = Path.Combine(appDir, "Recipes"); + var destPath = Path.Combine(destDir, recipeFileName); + + if (File.Exists(destPath)) + { + return; + } + + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith(recipeFileName, StringComparison.OrdinalIgnoreCase)); + + if (resourceName is null) + { + return; + } + + if (!Directory.Exists(destDir)) + { + Directory.CreateDirectory(destDir); + } + + using var stream = assembly.GetManifestResourceStream(resourceName); + using var fileStream = File.Create(destPath); + stream!.CopyTo(fileStream); + + Log($"Migrations recipe copied to {destDir}"); + } + + public static void DeleteMigrationsRecipe(string appDir) + { + var destDir = Path.Combine(appDir, "Recipes"); + var destPath = Path.Combine(destDir, "migrations.recipe.json"); + + if (File.Exists(destPath)) + { + File.Delete(destPath); + Log($"Migrations recipe deleted from {destDir}"); + } + + // Remove Recipes dir if empty. + if (Directory.Exists(destDir) && !Directory.EnumerateFileSystemEntries(destDir).Any()) + { + Directory.Delete(destDir); + } + } + + public static Process HostApp(string appDir, string assembly) + { + var binPath = Path.Combine("bin", "Release", _dotnetVersion, assembly); + var fullBinPath = Path.Combine(appDir, binPath); + + if (!File.Exists(fullBinPath)) + { + BuildApp(appDir); + } + + Log("Starting application..."); + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = binPath, + WorkingDirectory = appDir, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }, + }; + + process.StartInfo.EnvironmentVariables["ORCHARD_APP_DATA"] = "./App_Data_Tests"; + + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data) && + (e.Data.Contains("Exception") || e.Data.StartsWith("fail:", StringComparison.Ordinal))) + { + Console.Error.WriteLine($"[Server Error] {e.Data}"); + } + }; + + process.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + { + Console.Error.WriteLine($"[Server stderr] {e.Data}"); + } + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + return process; + } + + public static async Task WaitForReadyAsync(string baseUrl, int timeoutMs = 60000) + { + var start = DateTime.UtcNow; + Log($"Waiting for server at {baseUrl}..."); + + using var client = new HttpClient(); + + while ((DateTime.UtcNow - start).TotalMilliseconds < timeoutMs) + { + try + { + var response = await client.GetAsync(baseUrl); + var statusCode = (int)response.StatusCode; + if (response.IsSuccessStatusCode || statusCode == 302 || statusCode == 404) + { + Log("Server is ready."); + return; + } + } + catch + { + // Server not ready yet. + } + + await Task.Delay(1000); + } + + throw new TimeoutException($"Server at {baseUrl} did not become ready within {timeoutMs}ms."); + } + + public static void KillApp(Process process) + { + if (process is not null && !process.HasExited) + { + process.Kill(entireProcessTree: true); + Log("Server process killed."); + } + } + + private static void Log(string message) + { + Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] {message}"); + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/AuthHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/AuthHelper.cs new file mode 100644 index 00000000000..c250a7e66a8 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/AuthHelper.cs @@ -0,0 +1,23 @@ +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class AuthHelper +{ + public static async Task LoginAsync(IPage page, string prefix = "", OrchardConfig config = null) + { + config ??= TestUtils.DefaultConfig; + await page.GotoAsync($"{prefix}/login"); + + // If already logged in (redirected away from login), skip. + if (!page.Url.Contains("/login", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + await page.Locator("#LoginForm_UserName").FillAsync(config.Username); + await page.Locator("#LoginForm_Password").FillAsync(config.Password); + await page.Locator("button[type=\"submit\"]").ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/ButtonHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/ButtonHelper.cs new file mode 100644 index 00000000000..10cb2e5939b --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/ButtonHelper.cs @@ -0,0 +1,27 @@ +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class ButtonHelper +{ + public static Task ClickCreateAsync(IPage page) + => page.Locator(".btn.create").ClickAsync(); + + public static Task ClickSaveAsync(IPage page) + => page.Locator(".btn.save").ClickAsync(); + + public static Task ClickSaveContinueAsync(IPage page) + => page.Locator(".dropdown-item.save-continue").ClickAsync(); + + public static Task ClickCancelAsync(IPage page) + => page.Locator(".btn.cancel").ClickAsync(); + + public static Task ClickPublishAsync(IPage page) + => page.Locator(".btn.public").ClickAsync(); + + public static Task ClickPublishContinueAsync(IPage page) + => page.Locator(".dropdown-item.publish-continue").ClickAsync(); + + public static Task ClickModalOkAsync(IPage page) + => page.Locator("#modalOkButton").ClickAsync(); +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/ConfigurationHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/ConfigurationHelper.cs new file mode 100644 index 00000000000..b2667a4aec4 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/ConfigurationHelper.cs @@ -0,0 +1,15 @@ +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class ConfigurationHelper +{ + public static async Task SetPageSizeAsync(IPage page, string prefix, string size) + { + await page.GotoAsync($"{prefix}/Admin/Settings/general"); + await page.Locator("#ISite_PageSize").ClearAsync(); + await page.Locator("#ISite_PageSize").FillAsync(size); + await ButtonHelper.ClickSaveAsync(page); + await page.Locator(".message-success").WaitForAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/FeatureHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/FeatureHelper.cs new file mode 100644 index 00000000000..c2239751d74 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/FeatureHelper.cs @@ -0,0 +1,18 @@ +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class FeatureHelper +{ + public static async Task EnableFeatureAsync(IPage page, string prefix, string featureName) + { + await page.GotoAsync($"{prefix}/Admin/Features"); + await page.Locator($"#btn-enable-{featureName}").ClickAsync(); + } + + public static async Task DisableFeatureAsync(IPage page, string prefix, string featureName) + { + await page.GotoAsync($"{prefix}/Admin/Features"); + await page.Locator($"#btn-disable-{featureName}").ClickAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/OrchardTestFixture.cs b/test/OrchardCore.Tests.Functional/Helpers/OrchardTestFixture.cs new file mode 100644 index 00000000000..1bfa5e2b3ec --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/OrchardTestFixture.cs @@ -0,0 +1,91 @@ +using System.Diagnostics; +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public sealed class OrchardTestFixture : IAsyncDisposable +{ + private Process _serverProcess; + private IPlaywright _playwright; + private IBrowser _browser; + private bool _disposed; + + public string BaseUrl { get; private set; } + public IBrowser Browser => _browser; + + private static string ProjectRoot + => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + + private static bool IsMvc + => Environment.GetEnvironmentVariable("ORCHARD_APP") == "mvc"; + + private static string AppDir + => IsMvc + ? Path.Combine(ProjectRoot, "src", "OrchardCore.Mvc.Web") + : Path.Combine(ProjectRoot, "src", "OrchardCore.Cms.Web"); + + private static string Assembly + => IsMvc ? "OrchardCore.Mvc.Web.dll" : "OrchardCore.Cms.Web.dll"; + + public async Task InitializeAsync() + { + BaseUrl = Environment.GetEnvironmentVariable("ORCHARD_URL") ?? "http://localhost:5000"; + + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ORCHARD_EXTERNAL"))) + { + AppLifecycleHelper.DeleteAppData(AppDir); + + if (!IsMvc) + { + AppLifecycleHelper.CopyMigrationsRecipe(AppDir); + } + + _serverProcess = AppLifecycleHelper.HostApp(AppDir, Assembly); + await AppLifecycleHelper.WaitForReadyAsync(BaseUrl, 30_000); + } + + _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + Headless = true, + }); + } + + public async Task CreatePageAsync() + { + var context = await _browser.NewContextAsync(new BrowserNewContextOptions + { + BaseURL = BaseUrl, + }); + + return await context.NewPageAsync(); + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (_browser is not null) + { + await _browser.CloseAsync(); + } + + _playwright?.Dispose(); + + if (_serverProcess is not null) + { + AppLifecycleHelper.KillApp(_serverProcess); + } + + if (!IsMvc) + { + AppLifecycleHelper.DeleteMigrationsRecipe( + Path.Combine(ProjectRoot, "src", "OrchardCore.Cms.Web")); + } + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/SelectorHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/SelectorHelper.cs new file mode 100644 index 00000000000..0793d2f6af7 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/SelectorHelper.cs @@ -0,0 +1,20 @@ +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class SelectorHelper +{ + public static ILocator GetByCy(IPage page, string selector, bool exact = false) + { + return exact + ? page.Locator($"[data-cy=\"{selector}\"]") + : page.Locator($"[data-cy^=\"{selector}\"]"); + } + + public static ILocator FindByCy(ILocator locator, string selector, bool exact = false) + { + return exact + ? locator.Locator($"[data-cy=\"{selector}\"]") + : locator.Locator($"[data-cy^=\"{selector}\"]"); + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/TenantHelper.cs b/test/OrchardCore.Tests.Functional/Helpers/TenantHelper.cs new file mode 100644 index 00000000000..b49b94eaef3 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/TenantHelper.cs @@ -0,0 +1,86 @@ +using Microsoft.Playwright; + +namespace OrchardCore.Tests.Functional.Helpers; + +public static class TenantHelper +{ + public static async Task VisitTenantSetupPageAsync(IPage page, TenantInfo tenant) + { + await page.GotoAsync("/Admin/Tenants"); + await page.Locator($"#btn-setup-{tenant.Name}").ClickAsync(); + } + + public static async Task SiteSetupAsync(IPage page, TenantInfo tenant) + { + var config = TestUtils.DefaultConfig; + await page.Locator("#SiteName").FillAsync(tenant.Name); + + // Set recipe value directly. + var recipeName = page.Locator("#RecipeName"); + if (await recipeName.CountAsync() > 0) + { + await recipeName.EvaluateAsync( + "(el, val) => { el.value = val; el.dispatchEvent(new Event('change', { bubbles: true })); }", + tenant.SetupRecipe); + } + + // Set database provider to Sqlite if not already set. + var dbProvider = page.Locator("#DatabaseProvider"); + if (await dbProvider.CountAsync() > 0) + { + var currentValue = await dbProvider.InputValueAsync(); + if (string.IsNullOrEmpty(currentValue)) + { + await dbProvider.SelectOptionAsync("Sqlite"); + } + } + + await page.Locator("#UserName").FillAsync(config.Username); + await page.Locator("#Email").FillAsync(config.Email); + await page.Locator("#Password").FillAsync(config.Password); + await page.Locator("#PasswordConfirmation").FillAsync(config.Password); + await page.Locator("#SubmitButton").ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + public static async Task CreateTenantAsync(IPage page, TenantInfo tenant) + { + await page.GotoAsync("/Admin/Tenants"); + await page.Locator(".btn.create").First.ClickAsync(); + await page.Locator("#Name").FillAsync(tenant.Name); + await page.Locator("#Description").FillAsync($"Recipe: {tenant.SetupRecipe}. {tenant.Description}"); + await page.Locator("#RequestUrlPrefix").FillAsync(tenant.Prefix); + + // Select recipe if available in the dropdown, otherwise skip. + var recipeSelect = page.Locator("#RecipeName"); + var hasOption = await recipeSelect.Locator($"option[value=\"{tenant.SetupRecipe}\"]").CountAsync(); + if (hasOption > 0) + { + await recipeSelect.SelectOptionAsync(tenant.SetupRecipe); + } + + // Set database provider to Sqlite if not already set by environment variable. + var dbProvider = page.Locator("#DatabaseProvider"); + var currentValue = await dbProvider.InputValueAsync(); + if (string.IsNullOrEmpty(currentValue)) + { + await dbProvider.SelectOptionAsync("Sqlite"); + } + else + { + // If a provider is set (via env var), set the table prefix to the tenant name. + await page.Locator("#TablePrefix").FillAsync(tenant.Name); + } + + await page.Locator("button.create[type=\"submit\"]").ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + public static async Task NewTenantAsync(IPage page, TenantInfo tenant) + { + await AuthHelper.LoginAsync(page); + await CreateTenantAsync(page, tenant); + await VisitTenantSetupPageAsync(page, tenant); + await SiteSetupAsync(page, tenant); + } +} diff --git a/test/OrchardCore.Tests.Functional/Helpers/TestUtils.cs b/test/OrchardCore.Tests.Functional/Helpers/TestUtils.cs new file mode 100644 index 00000000000..01fddc5ac08 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Helpers/TestUtils.cs @@ -0,0 +1,56 @@ +namespace OrchardCore.Tests.Functional.Helpers; + +public sealed class OrchardConfig +{ + public string Username { get; set; } = "admin"; + public string Email { get; set; } = "admin@orchard.com"; + public string Password { get; set; } = "Orchard1!"; +} + +public sealed class TenantInfo +{ + public string Name { get; set; } = string.Empty; + public string Prefix { get; set; } = string.Empty; + public string SetupRecipe { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} + +public static class TestUtils +{ + public static readonly OrchardConfig DefaultConfig = new(); + + public static TenantInfo GenerateTenantInfo(string setupRecipeName, string description = "") + { + var now = DateTime.UtcNow; + var today = now.Date; + var ms = (long)(now - today).TotalMilliseconds; + var uniqueName = "t" + ToBase32(ms); + + return new TenantInfo + { + Name = uniqueName, + Prefix = uniqueName, + SetupRecipe = setupRecipeName, + Description = description, + }; + } + + private static string ToBase32(long value) + { + const string digits = "0123456789abcdefghijklmnopqrstuv"; + if (value == 0) + { + return "0"; + } + + var result = new char[13]; + var index = result.Length; + while (value > 0) + { + result[--index] = digits[(int)(value % 32)]; + value /= 32; + } + + return new string(result, index, result.Length - index); + } +} diff --git a/test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj b/test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj new file mode 100644 index 00000000000..b5e9408ca01 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/OrchardCore.Tests.Functional.csproj @@ -0,0 +1,22 @@ + + + + $(CommonTargetFrameworks) + Exe + false + $(NoWarn);CA1707 + + + + + + + + + + + + + + + diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/AgencyTests.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/AgencyTests.cs new file mode 100644 index 00000000000..f4290ac0828 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/AgencyTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public sealed class AgencyTests : IAsyncLifetime +{ + private readonly CmsSetupFixture _fixture; + private TenantInfo _tenant; + + public AgencyTests(CmsSetupFixture fixture) + { + _fixture = fixture; + } + + public async ValueTask InitializeAsync() + { + _tenant = TestUtils.GenerateTenantInfo("Agency"); + var page = await _fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, _tenant); + await page.CloseAsync(); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + [Fact] + public async Task DisplaysTheHomePageOfTheAgencyTheme() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync($"/{_tenant.Prefix}"); + await Assertions.Expect(page.Locator("#services")).ToContainTextAsync("Lorem ipsum dolor sit amet consectetur"); + await page.CloseAsync(); + } + + [Fact] + public async Task AgencyAdminLoginShouldWork() + { + var page = await _fixture.CreatePageAsync(); + await AuthHelper.LoginAsync(page, $"/{_tenant.Prefix}"); + await page.GotoAsync($"/{_tenant.Prefix}/Admin"); + await Assertions.Expect(page.Locator(".menu-admin")).ToHaveAttributeAsync("id", "adminMenu"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/BlogTests.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/BlogTests.cs new file mode 100644 index 00000000000..0bdabd63db3 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/BlogTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public sealed class BlogTests : IAsyncLifetime +{ + private readonly CmsSetupFixture _fixture; + private TenantInfo _tenant; + + public BlogTests(CmsSetupFixture fixture) + { + _fixture = fixture; + } + + public async ValueTask InitializeAsync() + { + _tenant = TestUtils.GenerateTenantInfo("Blog"); + var page = await _fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, _tenant); + await page.CloseAsync(); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + [Fact] + public async Task DisplaysTheHomePageOfTheBlogRecipe() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync($"/{_tenant.Prefix}"); + await Assertions.Expect(page.Locator(".subheading")).ToContainTextAsync("This is the description of your blog"); + await page.CloseAsync(); + } + + [Fact] + public async Task BlogAdminLoginShouldWork() + { + var page = await _fixture.CreatePageAsync(); + await AuthHelper.LoginAsync(page, $"/{_tenant.Prefix}"); + await page.GotoAsync($"/{_tenant.Prefix}/Admin"); + await Assertions.Expect(page.Locator(".menu-admin")).ToHaveAttributeAsync("id", "adminMenu"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/CmsSetupFixture.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/CmsSetupFixture.cs new file mode 100644 index 00000000000..d604e905468 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/CmsSetupFixture.cs @@ -0,0 +1,46 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; + +namespace OrchardCore.Tests.Functional; + +public sealed class CmsSetupFixture : IAsyncLifetime +{ + private readonly OrchardTestFixture _testFixture = new(); + + public IBrowser Browser => _testFixture.Browser; + public string BaseUrl => _testFixture.BaseUrl; + + public async ValueTask InitializeAsync() + { + await _testFixture.InitializeAsync(); + + // Perform the default SaaS tenant setup. + var page = await CreatePageAsync(); + try + { + await page.GotoAsync("/"); + await TenantHelper.SiteSetupAsync(page, new TenantInfo + { + Name = "Testing SaaS", + Prefix = string.Empty, + SetupRecipe = "SaaS", + }); + await AuthHelper.LoginAsync(page); + await ConfigurationHelper.SetPageSizeAsync(page, string.Empty, "100"); + } + finally + { + await page.CloseAsync(); + } + } + + public async Task CreatePageAsync() + { + return await _testFixture.CreatePageAsync(); + } + + public async ValueTask DisposeAsync() + { + await _testFixture.DisposeAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/CmsTestBase.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/CmsTestBase.cs new file mode 100644 index 00000000000..10ff8531eb0 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/CmsTestBase.cs @@ -0,0 +1,31 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public abstract class CmsTestBase : IAsyncDisposable +{ + protected CmsSetupFixture Fixture { get; } + + protected CmsTestBase(CmsSetupFixture fixture) + { + Fixture = fixture; + } + + protected async Task<(IPage Page, TenantInfo Tenant)> SetupTenantAsync(string recipeName) + { + var tenant = TestUtils.GenerateTenantInfo(recipeName); + var page = await Fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, tenant); + await page.CloseAsync(); + + return (await Fixture.CreatePageAsync(), tenant); + } + + public virtual ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/CmsTestCollection.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/CmsTestCollection.cs new file mode 100644 index 00000000000..e7b56df308f --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/CmsTestCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace OrchardCore.Tests.Functional; + +[CollectionDefinition(Name)] +public sealed class CmsTestCollection : ICollectionFixture +{ + public const string Name = "CMS"; +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/ComingSoonTests.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/ComingSoonTests.cs new file mode 100644 index 00000000000..94409c356f8 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/ComingSoonTests.cs @@ -0,0 +1,47 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public sealed class ComingSoonTests : IAsyncLifetime +{ + private readonly CmsSetupFixture _fixture; + private TenantInfo _tenant; + + public ComingSoonTests(CmsSetupFixture fixture) + { + _fixture = fixture; + } + + public async ValueTask InitializeAsync() + { + _tenant = TestUtils.GenerateTenantInfo("ComingSoon"); + var page = await _fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, _tenant); + await page.CloseAsync(); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + [Fact] + public async Task DisplaysTheHomePageOfTheComingSoonTheme() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync($"/{_tenant.Prefix}"); + await Assertions.Expect(page.Locator("h1")).ToContainTextAsync("Coming Soon"); + await Assertions.Expect(page.Locator("p")).ToContainTextAsync("We're working hard to finish the development of this site."); + await page.CloseAsync(); + } + + [Fact] + public async Task ComingSoonAdminLoginShouldWork() + { + var page = await _fixture.CreatePageAsync(); + await AuthHelper.LoginAsync(page, $"/{_tenant.Prefix}"); + await page.GotoAsync($"/{_tenant.Prefix}/Admin"); + await Assertions.Expect(page.Locator(".menu-admin")).ToHaveAttributeAsync("id", "adminMenu"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/HeadlessTests.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/HeadlessTests.cs new file mode 100644 index 00000000000..907c8bfa293 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/HeadlessTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public sealed class HeadlessTests : IAsyncLifetime +{ + private readonly CmsSetupFixture _fixture; + private TenantInfo _tenant; + + public HeadlessTests(CmsSetupFixture fixture) + { + _fixture = fixture; + } + + public async ValueTask InitializeAsync() + { + _tenant = TestUtils.GenerateTenantInfo("Headless"); + var page = await _fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, _tenant); + await page.CloseAsync(); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + [Fact] + public async Task DisplaysTheLoginScreenForTheHeadlessTheme() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync($"/{_tenant.Prefix}"); + await Assertions.Expect(page.Locator("h1")).ToContainTextAsync("Log in"); + await page.CloseAsync(); + } + + [Fact] + public async Task HeadlessAdminLoginShouldWork() + { + var page = await _fixture.CreatePageAsync(); + await AuthHelper.LoginAsync(page, $"/{_tenant.Prefix}"); + await page.GotoAsync($"/{_tenant.Prefix}/Admin"); + await Assertions.Expect(page.Locator(".menu-admin")).ToHaveAttributeAsync("id", "adminMenu"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/MigrationsTests.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/MigrationsTests.cs new file mode 100644 index 00000000000..1b8a37bc846 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/MigrationsTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public sealed class MigrationsTests : IAsyncLifetime +{ + private readonly CmsSetupFixture _fixture; + private TenantInfo _tenant; + + public MigrationsTests(CmsSetupFixture fixture) + { + _fixture = fixture; + } + + public async ValueTask InitializeAsync() + { + _tenant = TestUtils.GenerateTenantInfo("Migrations"); + var page = await _fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, _tenant); + await page.CloseAsync(); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + [Fact] + public async Task DisplaysTheHomePageOfTheMigrationsRecipe() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync($"/{_tenant.Prefix}"); + await Assertions.Expect(page.GetByText("Testing features having database migrations")).ToBeVisibleAsync(); + await page.CloseAsync(); + } + + [Fact] + public async Task MigrationsAdminLoginShouldWork() + { + var page = await _fixture.CreatePageAsync(); + await AuthHelper.LoginAsync(page, $"/{_tenant.Prefix}"); + await page.GotoAsync($"/{_tenant.Prefix}/Admin"); + await Assertions.Expect(page.Locator(".menu-admin")).ToHaveAttributeAsync("id", "adminMenu"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Cms/SaasTests.cs b/test/OrchardCore.Tests.Functional/Tests/Cms/SaasTests.cs new file mode 100644 index 00000000000..310d262f41a --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Cms/SaasTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Cms; + +[Collection(CmsTestCollection.Name)] +public sealed class SaasTests : IAsyncLifetime +{ + private readonly CmsSetupFixture _fixture; + private TenantInfo _tenant; + + public SaasTests(CmsSetupFixture fixture) + { + _fixture = fixture; + } + + public async ValueTask InitializeAsync() + { + _tenant = TestUtils.GenerateTenantInfo("SaaS"); + var page = await _fixture.CreatePageAsync(); + await TenantHelper.NewTenantAsync(page, _tenant); + await page.CloseAsync(); + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + [Fact] + public async Task DisplaysTheHomePageOfTheSaasTheme() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync($"/{_tenant.Prefix}"); + await Assertions.Expect(page.Locator("h4")).ToContainTextAsync("Welcome to Orchard Core, your site has been successfully set up."); + await page.CloseAsync(); + } + + [Fact] + public async Task SaasAdminLoginShouldWork() + { + var page = await _fixture.CreatePageAsync(); + await AuthHelper.LoginAsync(page, $"/{_tenant.Prefix}"); + await page.GotoAsync($"/{_tenant.Prefix}/Admin"); + await Assertions.Expect(page.Locator(".menu-admin")).ToHaveAttributeAsync("id", "adminMenu"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcSetupFixture.cs b/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcSetupFixture.cs new file mode 100644 index 00000000000..0455f7db1c7 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcSetupFixture.cs @@ -0,0 +1,27 @@ +using Microsoft.Playwright; +using OrchardCore.Tests.Functional.Helpers; + +namespace OrchardCore.Tests.Functional; + +public sealed class MvcSetupFixture : IAsyncLifetime +{ + private readonly OrchardTestFixture _testFixture = new(); + + public IBrowser Browser => _testFixture.Browser; + public string BaseUrl => _testFixture.BaseUrl; + + public async ValueTask InitializeAsync() + { + await _testFixture.InitializeAsync(); + } + + public async Task CreatePageAsync() + { + return await _testFixture.CreatePageAsync(); + } + + public async ValueTask DisposeAsync() + { + await _testFixture.DisposeAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcTestCollection.cs b/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcTestCollection.cs new file mode 100644 index 00000000000..ac4636ce8fc --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcTestCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace OrchardCore.Tests.Functional; + +[CollectionDefinition(Name)] +public sealed class MvcTestCollection : ICollectionFixture +{ + public const string Name = "MVC"; +} diff --git a/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcTests.cs b/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcTests.cs new file mode 100644 index 00000000000..e4a9a9f7c49 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Tests/Mvc/MvcTests.cs @@ -0,0 +1,24 @@ +using Microsoft.Playwright; +using Xunit; + +namespace OrchardCore.Tests.Functional.Tests.Mvc; + +[Collection(MvcTestCollection.Name)] +public sealed class MvcTests +{ + private readonly MvcSetupFixture _fixture; + + public MvcTests(MvcSetupFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task ShouldDisplayHelloWorld() + { + var page = await _fixture.CreatePageAsync(); + await page.GotoAsync("/"); + await Assertions.Expect(page.Locator("body")).ToContainTextAsync("Hello World"); + await page.CloseAsync(); + } +} diff --git a/test/OrchardCore.Tests.Functional/Usings.cs b/test/OrchardCore.Tests.Functional/Usings.cs new file mode 100644 index 00000000000..8e7cdba38f0 --- /dev/null +++ b/test/OrchardCore.Tests.Functional/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.Diagnostics; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Reflection; +global using System.Threading.Tasks; +global using Xunit; diff --git a/test/OrchardCore.Tests.Playwright/OrchardCore.Tests.Playwright.csproj b/test/OrchardCore.Tests.Playwright/OrchardCore.Tests.Playwright.csproj deleted file mode 100644 index 27883fb9f4e..00000000000 --- a/test/OrchardCore.Tests.Playwright/OrchardCore.Tests.Playwright.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - $(CommonTargetFrameworks) - - - false - - - - - - - - - - diff --git a/test/OrchardCore.Tests.Playwright/global-setup.ts b/test/OrchardCore.Tests.Playwright/global-setup.ts deleted file mode 100644 index ed83ba0da09..00000000000 --- a/test/OrchardCore.Tests.Playwright/global-setup.ts +++ /dev/null @@ -1,39 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import { deleteAppData, copyMigrationsRecipe, hostApp, waitForReady } from './helpers/app-lifecycle'; - -const isMvc = process.env.ORCHARD_APP === 'mvc'; -const projectRoot = path.resolve(__dirname, '..', '..'); - -const cmsAppDir = path.join(projectRoot, 'src', 'OrchardCore.Cms.Web'); -const mvcAppDir = path.join(projectRoot, 'src', 'OrchardCore.Mvc.Web'); -const fixturesDir = path.join(__dirname, 'fixtures'); - -const appDir = isMvc ? mvcAppDir : cmsAppDir; -const assembly = isMvc ? 'OrchardCore.Mvc.Web.dll' : 'OrchardCore.Cms.Web.dll'; -const baseUrl = process.env.ORCHARD_URL || 'http://localhost:5000'; - -async function globalSetup(): Promise { - // Skip app lifecycle if ORCHARD_EXTERNAL is set (server managed externally) - if (process.env.ORCHARD_EXTERNAL) { - console.log('ORCHARD_EXTERNAL is set, skipping app build/host.'); - return; - } - - deleteAppData(appDir); - - // Copy migrations recipe for CMS tests (after clean so it's not deleted) - if (!isMvc) { - copyMigrationsRecipe(fixturesDir, appDir); - } - - const server = hostApp(appDir, assembly); - - // Store server PID so global-teardown can kill it - const pidFile = path.join(__dirname, '.server-pid'); - fs.writeFileSync(pidFile, String(server.pid)); - - await waitForReady(baseUrl, 30_000); -} - -export default globalSetup; diff --git a/test/OrchardCore.Tests.Playwright/global-teardown.ts b/test/OrchardCore.Tests.Playwright/global-teardown.ts deleted file mode 100644 index 66bda988d05..00000000000 --- a/test/OrchardCore.Tests.Playwright/global-teardown.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import { deleteMigrationsRecipe } from './helpers/app-lifecycle'; - -const isMvc = process.env.ORCHARD_APP === 'mvc'; -const projectRoot = path.resolve(__dirname, '..', '..'); -const cmsAppDir = path.join(projectRoot, 'src', 'OrchardCore.Cms.Web'); - -async function globalTeardown(): Promise { - if (process.env.ORCHARD_EXTERNAL) { - return; - } - - const pidFile = path.join(__dirname, '.server-pid'); - if (fs.existsSync(pidFile)) { - const pid = parseInt(fs.readFileSync(pidFile, 'utf-8'), 10); - try { - process.kill(pid, 'SIGINT'); - console.log(`Server process ${pid} killed.`); - } catch { - console.log(`Server process ${pid} already exited.`); - } - fs.unlinkSync(pidFile); - } - - // Clean up the migrations recipe copied during global setup. - if (!isMvc) { - deleteMigrationsRecipe(cmsAppDir); - } -} - -export default globalTeardown; diff --git a/test/OrchardCore.Tests.Playwright/helpers/app-lifecycle.ts b/test/OrchardCore.Tests.Playwright/helpers/app-lifecycle.ts deleted file mode 100644 index 0ed61f15121..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/app-lifecycle.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { execSync, spawn, type ChildProcess } from 'child_process'; -import path from 'path'; -import fs from 'fs'; - -const DOTNET_VERSION = 'net10.0'; - -function log(msg: string): void { - const now = new Date().toLocaleTimeString(); - console.log(`[${now}] ${msg}`); -} - -export function buildApp(appDir: string): void { - log('Building application...'); - execSync(`dotnet build -c Release -f ${DOTNET_VERSION}`, { cwd: appDir, stdio: 'inherit' }); - log('Build complete.'); -} - -export function deleteAppData(appDir: string, dataDir: string = 'App_Data_Tests'): void { - const fullPath = path.join(appDir, dataDir); - if (fs.existsSync(fullPath)) { - fs.rmSync(fullPath, { recursive: true, force: true }); - log(`${fullPath} deleted`); - } -} - -export function copyMigrationsRecipe(sourceDir: string, appDir: string): void { - const recipeFileName = 'migrations.recipe.json'; - const sourcePath = path.join(sourceDir, recipeFileName); - const destDir = path.join(appDir, 'Recipes'); - const destPath = path.join(destDir, recipeFileName); - - if (!fs.existsSync(sourcePath) || fs.existsSync(destPath)) { - return; - } - - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - - fs.copyFileSync(sourcePath, destPath); - log(`Migrations recipe copied to ${destDir}`); -} - -export function deleteMigrationsRecipe(appDir: string): void { - const destDir = path.join(appDir, 'Recipes'); - const destPath = path.join(destDir, 'migrations.recipe.json'); - - if (fs.existsSync(destPath)) { - fs.unlinkSync(destPath); - log(`Migrations recipe deleted from ${destDir}`); - } - - // Remove Recipes dir if empty. - if (fs.existsSync(destDir) && fs.readdirSync(destDir).length === 0) { - fs.rmdirSync(destDir); - } -} - -export function hostApp(appDir: string, assembly: string): ChildProcess { - const binPath = path.join('bin', 'Release', DOTNET_VERSION, assembly); - - if (!fs.existsSync(path.join(appDir, binPath))) { - buildApp(appDir); - } - - log('Starting application...'); - - const server = spawn('dotnet', [binPath], { - cwd: appDir, - env: { - ...process.env, - ORCHARD_APP_DATA: './App_Data_Tests', - }, - }); - - server.stdout?.on('data', (data) => { - const msg = data.toString(); - if (msg.includes('Exception') || msg.startsWith('fail:')) { - console.error(`[Server Error] ${msg}`); - } - }); - - server.stderr?.on('data', (data) => { - console.error(`[Server stderr] ${data}`); - }); - - server.on('close', (code) => { - log(`Server process exited with code ${code}`); - }); - - return server; -} - -export async function waitForReady(baseUrl: string, timeoutMs: number = 60000): Promise { - const start = Date.now(); - log(`Waiting for server at ${baseUrl}...`); - - while (Date.now() - start < timeoutMs) { - try { - const response = await fetch(baseUrl); - if (response.ok || response.status === 302 || response.status === 404) { - log('Server is ready.'); - return; - } - } catch { - // Server not ready yet - } - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - throw new Error(`Server at ${baseUrl} did not become ready within ${timeoutMs}ms`); -} - -export function killApp(proc: ChildProcess): void { - if (proc && !proc.killed) { - proc.kill('SIGINT'); - log('Server process killed.'); - } -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/auth.ts b/test/OrchardCore.Tests.Playwright/helpers/auth.ts deleted file mode 100644 index 60216f2d6da..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/auth.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type Page } from '@playwright/test'; -import { defaultOrchardConfig, type OrchardConfig } from './utils'; - -export async function login(page: Page, options: { prefix?: string; config?: OrchardConfig } = {}): Promise { - const { prefix = '', config = defaultOrchardConfig } = options; - await page.goto(`${prefix}/login`); - - // If already logged in (redirected away from login), skip - if (!page.url().toLowerCase().includes('/login')) { - return; - } - - await page.locator('#LoginForm_UserName').fill(config.username); - await page.locator('#LoginForm_Password').fill(config.password); - await page.locator('button[type="submit"]').click(); - await page.waitForLoadState('networkidle'); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/buttons.ts b/test/OrchardCore.Tests.Playwright/helpers/buttons.ts deleted file mode 100644 index f770a26eabe..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/buttons.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type Page } from '@playwright/test'; - -export async function btnCreateClick(page: Page): Promise { - await page.locator('.btn.create').click(); -} - -export async function btnSaveClick(page: Page): Promise { - await page.locator('.btn.save').click(); -} - -export async function btnSaveContinueClick(page: Page): Promise { - await page.locator('.dropdown-item.save-continue').click(); -} - -export async function btnCancelClick(page: Page): Promise { - await page.locator('.btn.cancel').click(); -} - -export async function btnPublishClick(page: Page): Promise { - await page.locator('.btn.public').click(); -} - -export async function btnPublishContinueClick(page: Page): Promise { - await page.locator('.dropdown-item.publish-continue').click(); -} - -export async function btnModalOkClick(page: Page): Promise { - await page.locator('#modalOkButton').click(); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/configuration.ts b/test/OrchardCore.Tests.Playwright/helpers/configuration.ts deleted file mode 100644 index 73d00592cc7..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/configuration.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type Page } from '@playwright/test'; -import { btnSaveClick } from './buttons'; - -export async function setPageSize(page: Page, prefix: string, size: string): Promise { - await page.goto(`${prefix}/Admin/Settings/general`); - await page.locator('#ISite_PageSize').clear(); - await page.locator('#ISite_PageSize').fill(size); - await btnSaveClick(page); - await page.locator('.message-success').waitFor(); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/features.ts b/test/OrchardCore.Tests.Playwright/helpers/features.ts deleted file mode 100644 index 4429e990da4..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/features.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type Page } from '@playwright/test'; - -export async function enableFeature(page: Page, prefix: string, featureName: string): Promise { - await page.goto(`${prefix}/Admin/Features`); - await page.locator(`#btn-enable-${featureName}`).click(); -} - -export async function disableFeature(page: Page, prefix: string, featureName: string): Promise { - await page.goto(`${prefix}/Admin/Features`); - await page.locator(`#btn-disable-${featureName}`).click(); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/recipes.ts b/test/OrchardCore.Tests.Playwright/helpers/recipes.ts deleted file mode 100644 index 5004cd50c19..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/recipes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { type Page } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { btnModalOkClick } from './buttons'; -import fs from 'fs'; - -export async function runRecipe(page: Page, prefix: string, recipeName: string): Promise { - await page.goto(`${prefix}/Admin/Recipes`); - await page.locator(`#btn-run-${recipeName}`).click(); - await btnModalOkClick(page); -} - -export async function uploadRecipeJson(page: Page, prefix: string, fixturePath: string): Promise { - const data = JSON.parse(fs.readFileSync(fixturePath, 'utf-8')); - await page.goto(`${prefix}/Admin/DeploymentPlan/Import/Json`); - await page.locator('.CodeMirror').waitFor({ state: 'visible' }); - await page.evaluate((json: string) => { - const cm = document.querySelector('.CodeMirror') as any; - cm.CodeMirror.setValue(json); - }, JSON.stringify(data)); - await page.locator('.ta-content > form').evaluate((form: HTMLFormElement) => form.submit()); - await expect(page.locator('.message-success')).toContainText('Recipe imported'); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/selectors.ts b/test/OrchardCore.Tests.Playwright/helpers/selectors.ts deleted file mode 100644 index 007484591b4..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/selectors.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type Page, type Locator } from '@playwright/test'; - -export function getByCy(page: Page, selector: string, exact: boolean = false): Locator { - if (exact) { - return page.locator(`[data-cy="${selector}"]`); - } - return page.locator(`[data-cy^="${selector}"]`); -} - -export function findByCy(locator: Locator, selector: string, exact: boolean = false): Locator { - if (exact) { - return locator.locator(`[data-cy="${selector}"]`); - } - return locator.locator(`[data-cy^="${selector}"]`); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/tenants.ts b/test/OrchardCore.Tests.Playwright/helpers/tenants.ts deleted file mode 100644 index 3d0de25e212..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/tenants.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { type Page } from '@playwright/test'; -import { defaultOrchardConfig, type TenantInfo } from './utils'; -import { login } from './auth'; - -export async function visitTenantSetupPage(page: Page, tenant: TenantInfo): Promise { - await page.goto('/Admin/Tenants'); - await page.locator(`#btn-setup-${tenant.name}`).click(); -} - -export async function siteSetup(page: Page, tenant: TenantInfo): Promise { - const config = defaultOrchardConfig; - await page.locator('#SiteName').fill(tenant.name); - - // Set recipe value directly (hidden input or select depending on context) - const recipeName = page.locator('#RecipeName'); - if (await recipeName.count() > 0) { - // Use evaluate like Cypress's .val() to set the value without requiring an option match - await recipeName.evaluate((el: HTMLElement, val: string) => { - (el as HTMLInputElement | HTMLSelectElement).value = val; - el.dispatchEvent(new Event('change', { bubbles: true })); - }, tenant.setupRecipe); - } - - // Set database provider to Sqlite if not already set - const dbProvider = page.locator('#DatabaseProvider'); - if (await dbProvider.count() > 0) { - const currentValue = await dbProvider.inputValue(); - if (!currentValue) { - await dbProvider.selectOption('Sqlite'); - } - } - - await page.locator('#UserName').fill(config.username); - await page.locator('#Email').fill(config.email); - await page.locator('#Password').fill(config.password); - await page.locator('#PasswordConfirmation').fill(config.password); - await page.locator('#SubmitButton').click(); - await page.waitForLoadState('networkidle'); -} - -export async function createTenant(page: Page, tenant: TenantInfo): Promise { - await page.goto('/Admin/Tenants'); - await page.locator('.btn.create').first().click(); - await page.locator('#Name').fill(tenant.name); - await page.locator('#Description').fill(`Recipe: ${tenant.setupRecipe}. ${tenant.description || ''}`); - await page.locator('#RequestUrlPrefix').fill(tenant.prefix); - - // Select recipe if available in the dropdown, otherwise skip (will be set during setup) - const recipeSelect = page.locator('#RecipeName'); - const hasOption = await recipeSelect.locator(`option[value="${tenant.setupRecipe}"]`).count(); - if (hasOption > 0) { - await recipeSelect.selectOption(tenant.setupRecipe); - } - - // Set database provider to Sqlite if not already set by environment variable - const dbProvider = page.locator('#DatabaseProvider'); - const currentValue = await dbProvider.inputValue(); - if (!currentValue) { - await dbProvider.selectOption('Sqlite'); - } else { - // If a provider is set (via env var), set the table prefix to the tenant name - await page.locator('#TablePrefix').fill(tenant.name); - } - - await page.locator('button.create[type="submit"]').click(); - await page.waitForLoadState('networkidle'); -} - -export async function newTenant(page: Page, tenant: TenantInfo): Promise { - await login(page); - await createTenant(page, tenant); - await visitTenantSetupPage(page, tenant); - await siteSetup(page, tenant); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/urls.ts b/test/OrchardCore.Tests.Playwright/helpers/urls.ts deleted file mode 100644 index 27b7c9623d6..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/urls.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { type Page } from '@playwright/test'; - -export async function visitContentPage(page: Page, prefix: string, contentItemId: string): Promise { - await page.goto(`${prefix}/Contents/ContentItems/${contentItemId}`); -} diff --git a/test/OrchardCore.Tests.Playwright/helpers/utils.ts b/test/OrchardCore.Tests.Playwright/helpers/utils.ts deleted file mode 100644 index 9aa71143370..00000000000 --- a/test/OrchardCore.Tests.Playwright/helpers/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -export interface TenantInfo { - name: string; - prefix: string; - setupRecipe: string; - description?: string; -} - -export interface OrchardConfig { - username: string; - email: string; - password: string; -} - -export const defaultOrchardConfig: OrchardConfig = { - username: 'admin', - email: 'admin@orchard.com', - password: 'Orchard1!', -}; - -export function generateTenantInfo(setupRecipeName: string, description?: string): TenantInfo { - const date = new Date(); - const today = new Date(date.getFullYear(), date.getMonth(), date.getDate()); - const uniqueName = 't' + (date.getTime() - today.getTime()).toString(32); - return { - name: uniqueName, - prefix: uniqueName, - setupRecipe: setupRecipeName, - description, - }; -} diff --git a/test/OrchardCore.Tests.Playwright/package.json b/test/OrchardCore.Tests.Playwright/package.json deleted file mode 100644 index c609770d2d8..00000000000 --- a/test/OrchardCore.Tests.Playwright/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@orchardcore/tests-playwright", - "version": "1.0.0", - "private": true, - "scripts": { - "test": "playwright test", - "test:cms": "playwright test tests/cms/", - "test:mvc": "ORCHARD_APP=mvc playwright test tests/mvc/", - "test:ui": "ORCHARD_EXTERNAL=1 playwright test --ui", - "test:headed": "ORCHARD_EXTERNAL=1 playwright test --headed", - "test:debug": "ORCHARD_EXTERNAL=1 playwright test --debug", - "install:browsers": "playwright install chromium" - }, - "devDependencies": { - "@playwright/test": "^1.50.0" - } -} diff --git a/test/OrchardCore.Tests.Playwright/playwright.config.ts b/test/OrchardCore.Tests.Playwright/playwright.config.ts deleted file mode 100644 index 6c222ec40a1..00000000000 --- a/test/OrchardCore.Tests.Playwright/playwright.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - testDir: './tests', - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: 1, - reporter: 'html', - timeout: 120_000, - use: { - baseURL: process.env.ORCHARD_URL || 'http://localhost:5000', - screenshot: 'only-on-failure', - video: 'on-first-retry', - trace: 'on-first-retry', - browserName: 'chromium', - }, - globalSetup: './global-setup.ts', - globalTeardown: './global-teardown.ts', - projects: [ - { - name: 'saas-setup', - testMatch: 'tests/cms/saas-setup.spec.ts', - }, - { - name: 'cms', - testMatch: 'tests/cms/!(saas-setup).spec.ts', - dependencies: ['saas-setup'], - }, - { - name: 'mvc', - testMatch: 'tests/mvc/**/*.spec.ts', - }, - ], -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/agency.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/agency.spec.ts deleted file mode 100644 index 09ce00777c3..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/agency.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { generateTenantInfo, type TenantInfo } from '../../helpers/utils'; -import { newTenant } from '../../helpers/tenants'; -import { login } from '../../helpers/auth'; - -test.describe('Agency Tests', () => { - let tenant: TenantInfo; - - test.beforeAll(async ({ browser }) => { - tenant = generateTenantInfo('Agency'); - const page = await browser.newPage(); - await newTenant(page, tenant); - await page.close(); - }); - - test('Displays the home page of the Agency theme', async ({ page }) => { - await page.goto(`/${tenant.prefix}`); - await expect(page.locator('#services')).toContainText('Lorem ipsum dolor sit amet consectetur'); - }); - - test('Agency admin login should work', async ({ page }) => { - await login(page, { prefix: `/${tenant.prefix}` }); - await page.goto(`/${tenant.prefix}/Admin`); - await expect(page.locator('.menu-admin')).toHaveAttribute('id', 'adminMenu'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/blog.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/blog.spec.ts deleted file mode 100644 index 2c6b71012eb..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/blog.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { generateTenantInfo, type TenantInfo } from '../../helpers/utils'; -import { newTenant } from '../../helpers/tenants'; -import { login } from '../../helpers/auth'; - -test.describe('Blog Tests', () => { - let tenant: TenantInfo; - - test.beforeAll(async ({ browser }) => { - tenant = generateTenantInfo('Blog'); - const page = await browser.newPage(); - await newTenant(page, tenant); - await page.close(); - }); - - test('Displays the home page of the blog recipe', async ({ page }) => { - await page.goto(`/${tenant.prefix}`); - await expect(page.locator('.subheading')).toContainText('This is the description of your blog'); - }); - - test('Blog admin login should work', async ({ page }) => { - await login(page, { prefix: `/${tenant.prefix}` }); - await page.goto(`/${tenant.prefix}/Admin`); - await expect(page.locator('.menu-admin')).toHaveAttribute('id', 'adminMenu'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/comingsoon.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/comingsoon.spec.ts deleted file mode 100644 index 6dd93a0568b..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/comingsoon.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { generateTenantInfo, type TenantInfo } from '../../helpers/utils'; -import { newTenant } from '../../helpers/tenants'; -import { login } from '../../helpers/auth'; - -test.describe('ComingSoon Recipe test', () => { - let tenant: TenantInfo; - - test.beforeAll(async ({ browser }) => { - tenant = generateTenantInfo('ComingSoon'); - const page = await browser.newPage(); - await newTenant(page, tenant); - await page.close(); - }); - - test('Displays the home page of the ComingSoon theme', async ({ page }) => { - await page.goto(`/${tenant.prefix}`); - await expect(page.locator('h1')).toContainText('Coming Soon'); - await expect(page.locator('p')).toContainText("We're working hard to finish the development of this site."); - }); - - test('ComingSoon admin login should work', async ({ page }) => { - await login(page, { prefix: `/${tenant.prefix}` }); - await page.goto(`/${tenant.prefix}/Admin`); - await expect(page.locator('.menu-admin')).toHaveAttribute('id', 'adminMenu'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/headless.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/headless.spec.ts deleted file mode 100644 index 9a8fa7f853f..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/headless.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { generateTenantInfo, type TenantInfo } from '../../helpers/utils'; -import { newTenant } from '../../helpers/tenants'; -import { login } from '../../helpers/auth'; - -test.describe('Headless Recipe test', () => { - let tenant: TenantInfo; - - test.beforeAll(async ({ browser }) => { - tenant = generateTenantInfo('Headless'); - const page = await browser.newPage(); - await newTenant(page, tenant); - await page.close(); - }); - - test('Displays the login screen when accessing the root of the Headless theme', async ({ page }) => { - await page.goto(`/${tenant.prefix}`); - await expect(page.locator('h1')).toContainText('Log in'); - }); - - test('Headless admin login should work', async ({ page }) => { - await login(page, { prefix: `/${tenant.prefix}` }); - await page.goto(`/${tenant.prefix}/Admin`); - await expect(page.locator('.menu-admin')).toHaveAttribute('id', 'adminMenu'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/migrations.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/migrations.spec.ts deleted file mode 100644 index 0ad17d59010..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/migrations.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { generateTenantInfo, type TenantInfo } from '../../helpers/utils'; -import { newTenant } from '../../helpers/tenants'; -import { login } from '../../helpers/auth'; - -test.describe('Migrations Tests', () => { - let tenant: TenantInfo; - - test.beforeAll(async ({ browser }) => { - tenant = generateTenantInfo('Migrations'); - const page = await browser.newPage(); - await newTenant(page, tenant); - await page.close(); - }); - - test('Displays the home page of the migrations recipe', async ({ page }) => { - await page.goto(`/${tenant.prefix}`); - await expect(page.getByText('Testing features having database migrations')).toBeVisible(); - }); - - test('Migrations admin login should work', async ({ page }) => { - await login(page, { prefix: `/${tenant.prefix}` }); - await page.goto(`/${tenant.prefix}/Admin`); - await expect(page.locator('.menu-admin')).toHaveAttribute('id', 'adminMenu'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/saas-setup.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/saas-setup.spec.ts deleted file mode 100644 index cc0345d08b6..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/saas-setup.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { login } from '../../helpers/auth'; -import { siteSetup } from '../../helpers/tenants'; -import { setPageSize } from '../../helpers/configuration'; - -const sassSite = { - name: 'Testing SaaS', - prefix: '', - setupRecipe: 'SaaS', -}; - -test.describe('Setup SaaS', () => { - test('Successfully setup the SaaS default tenant', async ({ page }) => { - await page.goto('/'); - await siteSetup(page, sassSite); - await login(page, { prefix: sassSite.prefix }); - await setPageSize(page, sassSite.prefix, '100'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/cms/saas.spec.ts b/test/OrchardCore.Tests.Playwright/tests/cms/saas.spec.ts deleted file mode 100644 index bd78f1038ed..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/cms/saas.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { generateTenantInfo, type TenantInfo } from '../../helpers/utils'; -import { newTenant } from '../../helpers/tenants'; -import { login } from '../../helpers/auth'; - -test.describe('SaaS Recipe test', () => { - let tenant: TenantInfo; - - test.beforeAll(async ({ browser }) => { - tenant = generateTenantInfo('SaaS'); - const page = await browser.newPage(); - await newTenant(page, tenant); - await page.close(); - }); - - test('Displays the home page of the SaaS theme', async ({ page }) => { - await page.goto(`/${tenant.prefix}`); - await expect(page.locator('h4')).toContainText('Welcome to Orchard Core, your site has been successfully set up.'); - }); - - test('SaaS admin login should work', async ({ page }) => { - await login(page, { prefix: `/${tenant.prefix}` }); - await page.goto(`/${tenant.prefix}/Admin`); - await expect(page.locator('.menu-admin')).toHaveAttribute('id', 'adminMenu'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tests/mvc/mvc.spec.ts b/test/OrchardCore.Tests.Playwright/tests/mvc/mvc.spec.ts deleted file mode 100644 index 15ea6744ba9..00000000000 --- a/test/OrchardCore.Tests.Playwright/tests/mvc/mvc.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('MVC Tests', () => { - test('should display "Hello World"', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('body')).toContainText('Hello World'); - }); -}); diff --git a/test/OrchardCore.Tests.Playwright/tsconfig.json b/test/OrchardCore.Tests.Playwright/tsconfig.json deleted file mode 100644 index 32a88077941..00000000000 --- a/test/OrchardCore.Tests.Playwright/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true - }, - "include": ["tests/**/*.ts", "helpers/**/*.ts", "*.ts"] -}