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"]
-}