diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..ea520a77 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,209 @@ +name: Tests + +on: + push: + branches: [ develop, main ] + pull_request: + branches: [ develop, main ] + +concurrency: + group: integration-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests: + name: Integration Tests (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + rid: linux-x64 + - os: windows-2022 + rid: win-x64 + - os: macos-14 + rid: osx-arm64 + + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 + CI: true + ELECTRON_ENABLE_LOGGING: 1 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Restore + run: dotnet restore -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj + + - name: Build + run: dotnet build --no-restore -c Release -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj + + - name: Install Linux GUI dependencies + if: runner.os == 'Linux' + run: | + set -e + sudo apt-get update + # Core Electron dependencies + sudo apt-get install -y xvfb \ + libgtk-3-0 libnss3 libgdk-pixbuf-2.0-0 libdrm2 libgbm1 libxss1 libxtst6 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libx11-xcb1 libasound2t64 + + - name: Run tests (Linux) + if: runner.os == 'Linux' + continue-on-error: true + run: | + mkdir -p test-results/Ubuntu + xvfb-run -a dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj \ + -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} \ + --logger "trx;LogFileName=Ubuntu.trx" \ + --logger "console;verbosity=detailed" \ + --results-directory test-results + + - name: Run tests (Windows) + if: runner.os == 'Windows' + continue-on-error: true + run: | + New-Item -ItemType Directory -Force -Path test-results/Windows | Out-Null + dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} --logger "trx;LogFileName=Windows.trx" --logger "console;verbosity=detailed" --results-directory test-results + + - name: Run tests (macOS) + if: runner.os == 'macOS' + continue-on-error: true + run: | + mkdir -p test-results/macOS + dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} --logger "trx;LogFileName=macOS.trx" --logger "console;verbosity=detailed" --results-directory test-results + + - name: Upload raw test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.os }} + path: test-results/*.trx + retention-days: 7 + + summary: + name: Test Results + runs-on: ubuntu-24.04 + if: always() + needs: [tests] + + permissions: + actions: read + contents: read + checks: write + pull-requests: write + + steps: + - name: Download all test results + uses: actions/download-artifact@v4 + with: + path: test-results + + - name: Setup .NET (for CTRF conversion) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Install CTRF TRX→CTRF converter (dotnet tool) + run: | + dotnet new tool-manifest + dotnet tool install DotnetCtrfJsonReporter --local + + - name: Convert TRX → CTRF and clean names (keep suites; set filePath=OS) + shell: bash + run: | + set -euo pipefail + mkdir -p ctrf + shopt -s globstar nullglob + conv=0 + for trx in test-results/**/*.trx; do + fname="$(basename "$trx")" + os="${fname%.trx}" + outdir="ctrf/${os}" + mkdir -p "$outdir" + out="${outdir}/ctrf-report.json" + + dotnet tool run DotnetCtrfJsonReporter -p "$trx" -d "$outdir" -f "ctrf-report.json" + + jq --arg os "$os" '.results.tests |= map(.filePath = $os)' "$out" > "${out}.tmp" && mv "${out}.tmp" "$out" + + echo "Converted & normalized $trx -> $out" + conv=$((conv+1)) + done + echo "Processed $conv TRX file(s)" + + + - name: Publish Test Report + if: always() + uses: ctrf-io/github-test-reporter@v1 + with: + report-path: 'ctrf/**/*.json' + + summary: true + pull-request: false + status-check: false + status-check-name: 'Integration Tests' + use-suite-name: true + update-comment: true + always-group-by: true + overwrite-comment: true + exit-on-fail: true + group-by: 'suite' + upload-artifact: true + fetch-previous-results: true + + summary-report: false + summary-delta-report: true + github-report: true + test-report: false + test-list-report: false + failed-report: true + failed-folded-report: false + skipped-report: true + suite-folded-report: true + suite-list-report: false + file-report: true + previous-results-report: true + insights-report: true + flaky-report: true + flaky-rate-report: true + fail-rate-report: false + slowest-report: false + + report-order: 'summary-delta-report,failed-report,skipped-report,suite-folded-report,file-report,previous-results-report,github-report' + env: + GITHUB_TOKEN: ${{ github.token }} + + + - name: Create PR Comment + if: always() + uses: ctrf-io/github-test-reporter@v1 + with: + report-path: 'ctrf/**/*.json' + + summary: true + pull-request: true + use-suite-name: true + update-comment: true + always-group-by: true + overwrite-comment: true + upload-artifact: false + + pull-request-report: true + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Summary + run: echo "All matrix test jobs completed." \ No newline at end of file diff --git a/src/ElectronNET.API/ElectronNetRuntime.cs b/src/ElectronNET.API/ElectronNetRuntime.cs index 3f6fb434..78d976e8 100644 --- a/src/ElectronNET.API/ElectronNetRuntime.cs +++ b/src/ElectronNET.API/ElectronNetRuntime.cs @@ -24,6 +24,8 @@ static ElectronNetRuntime() StartupManager.Initialize(); } + public static string ElectronExtraArguments { get; set; } + public static int? ElectronSocketPort { get; internal set; } public static int? AspNetWebPort { get; internal set; } diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs index 8e1633dd..70591674 100644 --- a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs +++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs @@ -40,7 +40,7 @@ protected override Task StartCore() { var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged(); var electronBinaryName = ElectronNetRuntime.ElectronExecutable; - var args = Environment.CommandLine; + var args = string.Format("{0} {1}", ElectronNetRuntime.ElectronExtraArguments, Environment.CommandLine).Trim(); this.port = ElectronNetRuntime.ElectronSocketPort; if (!this.port.HasValue) @@ -49,10 +49,15 @@ protected override Task StartCore() ElectronNetRuntime.ElectronSocketPort = this.port; } + Console.Error.WriteLine("[StartCore]: isUnPacked: {0}", isUnPacked); + Console.Error.WriteLine("[StartCore]: electronBinaryName: {0}", electronBinaryName); + Console.Error.WriteLine("[StartCore]: args: {0}", args); + this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, this.port.Value); this.electronProcess.Ready += this.ElectronProcess_Ready; this.electronProcess.Stopped += this.ElectronProcess_Stopped; + Console.Error.WriteLine("[StartCore]: Before Start"); return this.electronProcess.Start(); } @@ -82,11 +87,11 @@ private void ElectronProcess_Stopped(object sender, EventArgs e) private void HandleStopped() { - if (this.socketBridge.State != LifetimeState.Stopped) + if (this.socketBridge != null && this.socketBridge.State != LifetimeState.Stopped) { this.socketBridge.Stop(); } - else if (this.electronProcess.State != LifetimeState.Stopped) + else if (this.electronProcess != null && this.electronProcess.State != LifetimeState.Stopped) { this.electronProcess.Stop(); } diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs index 297c17c5..0cf21c54 100644 --- a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs +++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel; using System.IO; + using System.Runtime.InteropServices; using System.Threading.Tasks; /// @@ -42,6 +43,11 @@ protected override Task StartCore() var electrondir = Path.Combine(dir.FullName, ".electron"); startCmd = Path.Combine(electrondir, "node_modules", "electron", "dist", "electron"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + startCmd = Path.Combine(electrondir, "node_modules", "electron", "dist", "Electron.app", "Contents", "MacOS", "Electron"); + } + args = $"main.js -unpackeddotnet --trace-warnings -electronforcedport={this.socketPort:D} " + this.extraArguments; workingDir = electrondir; } @@ -68,22 +74,40 @@ protected override Task StopCore() private async Task StartInternal(string startCmd, string args, string directoriy) { - await Task.Delay(10).ConfigureAwait(false); + try + { + await Task.Delay(10).ConfigureAwait(false); - this.process = new ProcessRunner("ElectronRunner"); - this.process.ProcessExited += this.Process_Exited; - this.process.Run(startCmd, args, directoriy); + Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd); + Console.Error.WriteLine("[StartInternal]: args: {0}", args); - await Task.Delay(500).ConfigureAwait(false); + this.process = new ProcessRunner("ElectronRunner"); + this.process.ProcessExited += this.Process_Exited; + this.process.Run(startCmd, args, directoriy); - if (!this.process.IsRunning) - { - Task.Run(() => this.TransitionState(LifetimeState.Stopped)); + await Task.Delay(500).ConfigureAwait(false); - throw new Exception("Failed to launch the Electron process."); - } + Console.Error.WriteLine("[StartInternal]: after run:"); + + if (!this.process.IsRunning) + { + Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardError); + Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardOutput); - this.TransitionState(LifetimeState.Ready); + Task.Run(() => this.TransitionState(LifetimeState.Stopped)); + + throw new Exception("Failed to launch the Electron process."); + } + + this.TransitionState(LifetimeState.Ready); + } + catch (Exception ex) + { + Console.Error.WriteLine("[StartInternal]: Exception: " + this.process?.StandardError); + Console.Error.WriteLine("[StartInternal]: Exception: " + this.process?.StandardOutput); + Console.Error.WriteLine("[StartInternal]: Exception: " + ex); + throw; + } } private void Process_Exited(object sender, EventArgs e) diff --git a/src/ElectronNET.API/Runtime/StartupManager.cs b/src/ElectronNET.API/Runtime/StartupManager.cs index 607d58b9..125b6dee 100644 --- a/src/ElectronNET.API/Runtime/StartupManager.cs +++ b/src/ElectronNET.API/Runtime/StartupManager.cs @@ -132,13 +132,19 @@ private BuildInfo GatherBuildInfo() if (electronAssembly == null) { + Console.WriteLine("GatherBuildInfo: Early exit"); return buildInfo; } if (electronAssembly.GetName().Name == "testhost" || electronAssembly.GetName().Name == "ReSharperTestRunner") { + Console.WriteLine("GatherBuildInfo: Detected testhost"); electronAssembly = AppDomain.CurrentDomain.GetData("ElectronTestAssembly") as Assembly ?? electronAssembly; } + else + { + Console.WriteLine("GatherBuildInfo: No testhost detected: " + electronAssembly.GetName().Name); + } var attributes = electronAssembly.GetCustomAttributes().ToList(); diff --git a/src/ElectronNET.IntegrationTests/Common/SkipOnWslFactAttribute.cs b/src/ElectronNET.IntegrationTests/Common/SkipOnWslFactAttribute.cs new file mode 100644 index 00000000..8c1a3d02 --- /dev/null +++ b/src/ElectronNET.IntegrationTests/Common/SkipOnWslFactAttribute.cs @@ -0,0 +1,49 @@ +namespace ElectronNET.IntegrationTests.Common +{ + using System.Runtime.InteropServices; + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class SkipOnWslFactAttribute : FactAttribute + { + private static readonly bool IsOnWsl; + + static SkipOnWslFactAttribute() + { + IsOnWsl = DetectWsl(); + } + + /// + /// Initializes a new instance of the class. + /// + public SkipOnWslFactAttribute() + { + if (IsOnWsl) + { + this.Skip = "Skipping test on WSL environment."; + } + } + + private static bool DetectWsl() + { + try + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return false; + } + + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_DISTRO_NAME")) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_INTEROP"))) + { + return true; + } + + return false; + } + catch + { + return false; + } + } + } +} diff --git a/src/ElectronNET.IntegrationTests/ElectronFixture.cs b/src/ElectronNET.IntegrationTests/ElectronFixture.cs index 68a1ec9b..cfaf8df2 100644 --- a/src/ElectronNET.IntegrationTests/ElectronFixture.cs +++ b/src/ElectronNET.IntegrationTests/ElectronFixture.cs @@ -1,38 +1,65 @@ namespace ElectronNET.IntegrationTests { + using System.Diagnostics.CodeAnalysis; using System.Reflection; using ElectronNET.API; using ElectronNET.API.Entities; // Shared fixture that starts Electron runtime once + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] public class ElectronFixture : IAsyncLifetime { public BrowserWindow MainWindow { get; private set; } = null!; public async Task InitializeAsync() { - AppDomain.CurrentDomain.SetData("ElectronTestAssembly", Assembly.GetExecutingAssembly()); - var runtimeController = ElectronNetRuntime.RuntimeController; - await runtimeController.Start(); - await runtimeController.WaitReadyTask; - - // create hidden window for tests (avoid showing UI) - this.MainWindow = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions + try { - Show = false, - Width = 800, - Height = 600, - }); + Console.Error.WriteLine("[ElectronFixture] InitializeAsync: start"); + AppDomain.CurrentDomain.SetData("ElectronTestAssembly", Assembly.GetExecutingAssembly()); + + Console.WriteLine("[ElectronFixture] Acquire RuntimeController"); + var runtimeController = ElectronNetRuntime.RuntimeController; + ElectronNetRuntime.ElectronExtraArguments = "--no-sandbox"; + + Console.Error.WriteLine("[ElectronFixture] Starting Electron runtime..."); + await runtimeController.Start(); + + Console.Error.WriteLine("[ElectronFixture] Waiting for Ready..."); + await Task.WhenAny(runtimeController.WaitReadyTask, Task.Delay(TimeSpan.FromSeconds(10))); - // Clear potential cache side-effects - await this.MainWindow.WebContents.Session.ClearCacheAsync(); + if (!runtimeController.WaitReadyTask.IsCompleted) + { + throw new TimeoutException("The Electron process did not start within 10 seconds"); + } + + Console.Error.WriteLine("[ElectronFixture] Runtime Ready"); + + // create hidden window for tests (avoid showing UI) + this.MainWindow = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions + { + Show = false, + Width = 800, + Height = 600, + }, "about:blank"); + + await this.MainWindow.WebContents.Session.ClearCacheAsync(); + } + catch (Exception ex) + { + Console.Error.WriteLine("[ElectronFixture] InitializeAsync: exception"); + Console.Error.WriteLine(ex.ToString()); + throw; + } } public async Task DisposeAsync() { var runtimeController = ElectronNetRuntime.RuntimeController; + Console.Error.WriteLine("[ElectronFixture] Stopping Electron runtime..."); await runtimeController.Stop(); await runtimeController.WaitStoppedTask; + Console.Error.WriteLine("[ElectronFixture] Runtime stopped"); } } diff --git a/src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj b/src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj index 77932967..c12d1e57 100644 --- a/src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj +++ b/src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj @@ -8,7 +8,7 @@ - net8.0 + net10.0 enable enable false @@ -16,14 +16,14 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -37,6 +37,12 @@ false + + + PreserveNewest + + + diff --git a/src/ElectronNET.IntegrationTests/Tests/AppTests.cs b/src/ElectronNET.IntegrationTests/Tests/AppTests.cs index 7658b623..1a15efdb 100644 --- a/src/ElectronNET.IntegrationTests/Tests/AppTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/AppTests.cs @@ -1,10 +1,10 @@ namespace ElectronNET.IntegrationTests.Tests { - using System.Runtime.InteropServices; using ElectronNET.API; using ElectronNET.API.Entities; using System; using System.IO; + using System.Runtime.Versioning; using System.Threading.Tasks; [Collection("ElectronCollection")] @@ -47,28 +47,6 @@ public async Task Can_get_special_paths() Directory.Exists(temp).Should().BeTrue(); } - - [Fact(Timeout = 20000)] - public async Task Badge_count_roundtrip_where_supported() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - var ok = await Electron.App.SetBadgeCountAsync(3); - ok.Should().BeTrue(); - var count = await Electron.App.GetBadgeCountAsync(); - count.Should().Be(3); - // reset - await Electron.App.SetBadgeCountAsync(0); - } - else - { - // On Windows it's usually unsupported; ensure badge query works and returns a non-negative value - await Electron.App.SetBadgeCountAsync(0); // ignore return value - var count = await Electron.App.GetBadgeCountAsync(); - count.Should().BeGreaterOrEqualTo(0); - } - } - [Fact(Timeout = 20000)] public async Task Can_get_app_metrics() { @@ -84,21 +62,15 @@ public async Task Can_get_gpu_feature_status() status.Should().NotBeNull(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] + [SupportedOSPlatform("Windows")] public async Task Can_get_login_item_settings() { var settings = await Electron.App.GetLoginItemSettingsAsync(); settings.Should().NotBeNull(); } - [Fact(Timeout = 20000)] - public async Task Can_set_app_logs_path() - { - var tempDir = Path.Combine(Path.GetTempPath(), "ElectronLogsTest" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(tempDir); - Electron.App.SetAppLogsPath(tempDir); - } - [Fact(Timeout = 20000)] public async Task CommandLine_append_and_query_switch() { @@ -108,7 +80,9 @@ public async Task CommandLine_append_and_query_switch() (await Electron.App.CommandLine.GetSwitchValueAsync(switchName)).Should().Be("value123"); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] + [SupportedOSPlatform("Windows")] public async Task Accessibility_support_toggle() { Electron.App.SetAccessibilitySupportEnabled(true); @@ -127,13 +101,15 @@ public async Task UserAgentFallback_roundtrip() Electron.App.UserAgentFallback = original; // restore } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("Linux")] + [SupportedOSPlatform("macOS")] public async Task BadgeCount_set_and_reset_where_supported() { await Electron.App.SetBadgeCountAsync(2); var count = await Electron.App.GetBadgeCountAsync(); // Some platforms may always return0; just ensure call didn't throw and is non-negative - count.Should().BeGreaterOrEqualTo(0); + count.Should().BeGreaterThanOrEqualTo(0); await Electron.App.SetBadgeCountAsync(0); } @@ -144,17 +120,6 @@ public async Task App_metrics_have_cpu_info() metrics[0].Cpu.Should().NotBeNull(); } - [Fact(Timeout = 20000)] - public async Task App_badge_count_roundtrip() - { - // Set then get (non-mac platforms treat as no-op but should return0 or set value) - var success = await Electron.App.SetBadgeCountAsync(3); - success.Should().BeTrue(); - var count = await Electron.App.GetBadgeCountAsync(); - // Allow fallback to0 on platforms without badge support - (count == 3 || count == 0).Should().BeTrue(); - } - [Fact(Timeout = 20000)] public async Task App_gpu_feature_status_has_some_fields() { @@ -164,4 +129,4 @@ public async Task App_gpu_feature_status_has_some_fields() status.VideoDecode.Should().NotBeNull(); } } -} \ No newline at end of file +} diff --git a/src/ElectronNET.IntegrationTests/Tests/AutoUpdaterTests.cs b/src/ElectronNET.IntegrationTests/Tests/AutoUpdaterTests.cs index 07138323..1bcb3c04 100644 --- a/src/ElectronNET.IntegrationTests/Tests/AutoUpdaterTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/AutoUpdaterTests.cs @@ -89,6 +89,7 @@ public async Task ChannelAsync_check() var test = await Electron.AutoUpdater.ChannelAsync; test.Should().Be(string.Empty); Electron.AutoUpdater.SetChannel = "beta"; + await Task.Delay(500); test = await Electron.AutoUpdater.ChannelAsync; test.Should().Be("beta"); } diff --git a/src/ElectronNET.IntegrationTests/Tests/BrowserWindowTests.cs b/src/ElectronNET.IntegrationTests/Tests/BrowserWindowTests.cs index e7e69021..b6c8bcf8 100644 --- a/src/ElectronNET.IntegrationTests/Tests/BrowserWindowTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/BrowserWindowTests.cs @@ -1,8 +1,10 @@ namespace ElectronNET.IntegrationTests.Tests { using System.Runtime.InteropServices; + using System.Runtime.Versioning; using ElectronNET.API; using ElectronNET.API.Entities; + using ElectronNET.IntegrationTests.Common; [Collection("ElectronCollection")] public class BrowserWindowTests @@ -42,7 +44,7 @@ public async Task Can_set_progress_bar_and_clear() await Task.Delay(50); } - [Fact(Timeout = 20000)] + [SkipOnWslFact(Timeout = 20000)] public async Task Can_set_and_get_position() { this.fx.MainWindow.SetPosition(134, 246); @@ -91,21 +93,26 @@ public async Task AlwaysOnTop_toggle_and_query() (await this.fx.MainWindow.IsAlwaysOnTopAsync()).Should().BeFalse(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("Linux")] + [SupportedOSPlatform("Windows")] public async Task MenuBar_auto_hide_and_visibility() { this.fx.MainWindow.SetAutoHideMenuBar(true); + await Task.Delay(500); (await this.fx.MainWindow.IsMenuBarAutoHideAsync()).Should().BeTrue(); this.fx.MainWindow.SetMenuBarVisibility(false); + await Task.Delay(500); (await this.fx.MainWindow.IsMenuBarVisibleAsync()).Should().BeFalse(); this.fx.MainWindow.SetMenuBarVisibility(true); + await Task.Delay(500); (await this.fx.MainWindow.IsMenuBarVisibleAsync()).Should().BeTrue(); } [Fact(Timeout = 20000)] public async Task ReadyToShow_event_fires_after_content_ready() { - var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = false }); + var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = false }, "about:blank"); var tcs = new TaskCompletionSource(); window.OnReadyToShow += () => tcs.TrySetResult(); @@ -125,7 +132,7 @@ public async Task ReadyToShow_event_fires_after_content_ready() [Fact(Timeout = 20000)] public async Task PageTitleUpdated_event_fires_on_title_change() { - var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = true }); + var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = true }, "about:blank"); var tcs = new TaskCompletionSource(); window.OnPageTitleUpdated += title => tcs.TrySetResult(title); @@ -145,7 +152,7 @@ public async Task PageTitleUpdated_event_fires_on_title_change() [Fact(Timeout = 20000)] public async Task Resize_event_fires_on_size_change() { - var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = false }); + var window = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = false }, "about:blank"); var resized = false; window.OnResize += () => resized = true; window.SetSize(500, 400); @@ -165,7 +172,9 @@ public async Task Progress_bar_and_always_on_top_toggle() (await win.IsAlwaysOnTopAsync()).Should().BeFalse(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("Linux")] + [SupportedOSPlatform("Windows")] public async Task Menu_bar_visibility_and_auto_hide() { var win = this.fx.MainWindow; @@ -178,7 +187,7 @@ public async Task Menu_bar_visibility_and_auto_hide() [Fact(Timeout = 20000)] public async Task Parent_child_relationship_roundtrip() { - var child = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = false, Width = 300, Height = 200 }); + var child = await Electron.WindowManager.CreateWindowAsync(new BrowserWindowOptions { Show = false, Width = 300, Height = 200 }, "about:blank"); this.fx.MainWindow.SetParentWindow(null); // ensure top-level child.SetParentWindow(this.fx.MainWindow); var parent = await child.GetParentWindowAsync(); @@ -188,36 +197,28 @@ public async Task Parent_child_relationship_roundtrip() child.Destroy(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] public async Task Represented_filename_and_edited_flags() { var win = this.fx.MainWindow; var temp = Path.Combine(Path.GetTempPath(), "electronnet_test.txt"); File.WriteAllText(temp, "test"); win.SetRepresentedFilename(temp); + + await Task.Delay(500); + var represented = await win.GetRepresentedFilenameAsync(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - represented.Should().Be(temp); - } - else - { - // Non-macOS platforms may not support represented filename; empty is acceptable - represented.Should().BeEmpty(); - } + represented.Should().Be(temp); win.SetDocumentEdited(true); + + await Task.Delay(500); + var edited = await win.IsDocumentEditedAsync(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - edited.Should().BeTrue(); - } - else - { - edited.Should().BeFalse(); // unsupported on non-mac platforms - } + edited.Should().BeTrue(); win.SetDocumentEdited(false); } } -} \ No newline at end of file +} diff --git a/src/ElectronNET.IntegrationTests/Tests/ClipboardTests.cs b/src/ElectronNET.IntegrationTests/Tests/ClipboardTests.cs index b3bd2dd1..17ff0dd6 100644 --- a/src/ElectronNET.IntegrationTests/Tests/ClipboardTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/ClipboardTests.cs @@ -1,5 +1,6 @@ namespace ElectronNET.IntegrationTests.Tests { + using System.Runtime.Versioning; using ElectronNET.API; [Collection("ElectronCollection")] @@ -31,7 +32,9 @@ public async Task Available_formats_contains_text_after_write() formats.Should().Contain(f => f.Contains("text") || f.Contains("TEXT") || f.Contains("plain")); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] + [SupportedOSPlatform("Windows")] public async Task Bookmark_write_and_read() { var url = "https://electron-test.com"; @@ -40,4 +43,4 @@ public async Task Bookmark_write_and_read() bookmark.Url.Should().Be(url); } } -} \ No newline at end of file +} diff --git a/src/ElectronNET.IntegrationTests/Tests/NativeThemeTests.cs b/src/ElectronNET.IntegrationTests/Tests/NativeThemeTests.cs index 16f6f44e..4128a223 100644 --- a/src/ElectronNET.IntegrationTests/Tests/NativeThemeTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/NativeThemeTests.cs @@ -1,5 +1,6 @@ namespace ElectronNET.IntegrationTests.Tests { + using System.Runtime.Versioning; using ElectronNET.API; using ElectronNET.API.Entities; @@ -12,15 +13,19 @@ public async Task ThemeSource_roundtrip() // Capture initial _ = await Electron.NativeTheme.ShouldUseDarkColorsAsync(); // Force light + await Task.Delay(50); Electron.NativeTheme.SetThemeSource(ThemeSourceMode.Light); + await Task.Delay(500); var useDarkAfterLight = await Electron.NativeTheme.ShouldUseDarkColorsAsync(); var themeSourceLight = await Electron.NativeTheme.GetThemeSourceAsync(); // Force dark Electron.NativeTheme.SetThemeSource(ThemeSourceMode.Dark); + await Task.Delay(500); var useDarkAfterDark = await Electron.NativeTheme.ShouldUseDarkColorsAsync(); var themeSourceDark = await Electron.NativeTheme.GetThemeSourceAsync(); // Restore system Electron.NativeTheme.SetThemeSource(ThemeSourceMode.System); + await Task.Delay(500); var themeSourceSystem = await Electron.NativeTheme.GetThemeSourceAsync(); // Assertions are tolerant (platform dependent) useDarkAfterLight.Should().BeFalse("forcing Light should result in light colors"); @@ -46,18 +51,22 @@ public async Task Updated_event_fires_on_change() fired.Should().BeTrue(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] + [SupportedOSPlatform("Windows")] public async Task Should_use_high_contrast_colors_check() { var metrics = await Electron.NativeTheme.ShouldUseHighContrastColorsAsync(); metrics.Should().Be(false); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] + [SupportedOSPlatform("Windows")] public async Task Should_use_inverted_colors_check() { var metrics = await Electron.NativeTheme.ShouldUseInvertedColorSchemeAsync(); metrics.Should().Be(false); } } -} \ No newline at end of file +} diff --git a/src/ElectronNET.IntegrationTests/Tests/NotificationTests.cs b/src/ElectronNET.IntegrationTests/Tests/NotificationTests.cs index c192dcea..a974bdc2 100644 --- a/src/ElectronNET.IntegrationTests/Tests/NotificationTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/NotificationTests.cs @@ -1,19 +1,24 @@ namespace ElectronNET.IntegrationTests.Tests { + using System.Runtime.InteropServices; using ElectronNET.API; using ElectronNET.API.Entities; [Collection("ElectronCollection")] public class NotificationTests { - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] public async Task Notification_create_check() { + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Linux), "Always returns false. Might need full-blown desktop environment"); + var tcs = new TaskCompletionSource(); var options = new NotificationOptions("Notification Title", "Notification test 123"); options.OnShow = () => tcs.SetResult(); + await Task.Delay(500); + Electron.Notification.Show(options); await Task.WhenAny(tcs.Task, Task.Delay(5_000)); @@ -21,9 +26,11 @@ public async Task Notification_create_check() tcs.Task.IsCompletedSuccessfully.Should().BeTrue(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] public async Task Notification_is_supported_check() { + Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.Linux), "Always returns false. Might need full-blown desktop environment"); + var supported = await Electron.Notification.IsSupportedAsync(); supported.Should().BeTrue(); } diff --git a/src/ElectronNET.IntegrationTests/Tests/ProcessTests.cs b/src/ElectronNET.IntegrationTests/Tests/ProcessTests.cs index 6a8eafd8..3fe27248 100644 --- a/src/ElectronNET.IntegrationTests/Tests/ProcessTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/ProcessTests.cs @@ -9,7 +9,7 @@ public class ProcessTests public async Task Process_info_is_accessible() { // Use renderer to fetch process info and round-trip - var execPath = await Electron.WindowManager.CreateWindowAsync(new API.Entities.BrowserWindowOptions { Show = false }); + var execPath = await Electron.WindowManager.CreateWindowAsync(new API.Entities.BrowserWindowOptions { Show = false }, "about:blank"); var result = await execPath.WebContents.ExecuteJavaScriptAsync("process.execPath && process.platform ? 'ok' : 'fail'"); result.Should().Be("ok"); } diff --git a/src/ElectronNET.IntegrationTests/Tests/ScreenTests.cs b/src/ElectronNET.IntegrationTests/Tests/ScreenTests.cs index 5ac585df..2ecf9ecb 100644 --- a/src/ElectronNET.IntegrationTests/Tests/ScreenTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/ScreenTests.cs @@ -1,8 +1,9 @@ -using ElectronNET.API.Entities; - namespace ElectronNET.IntegrationTests.Tests { + using System.Runtime.Versioning; using ElectronNET.API; + using ElectronNET.API.Entities; + using ElectronNET.IntegrationTests.Common; [Collection("ElectronCollection")] public class ScreenTests @@ -15,7 +16,7 @@ public ScreenTests(ElectronFixture fx) this.fx = fx; } - [Fact(Timeout = 20000)] + [SkipOnWslFact(Timeout = 20000)] public async Task Primary_display_has_positive_dimensions() { var display = await Electron.Screen.GetPrimaryDisplayAsync(); @@ -23,7 +24,7 @@ public async Task Primary_display_has_positive_dimensions() display.Size.Height.Should().BeGreaterThan(0); } - [Fact(Timeout = 20000)] + [SkipOnWslFact(Timeout = 20000)] public async Task GetAllDisplays_returns_at_least_one() { var displays = await Electron.Screen.GetAllDisplaysAsync(); @@ -40,7 +41,8 @@ public async Task GetCursorScreenPoint_check() point.Y.Should().BeGreaterThanOrEqualTo(0); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("macOS")] public async Task GetMenuBarWorkArea_check() { var area = await Electron.Screen.GetMenuBarWorkAreaAsync(); @@ -51,7 +53,7 @@ public async Task GetMenuBarWorkArea_check() area.Width.Should().BeGreaterThan(0); } - [Fact(Timeout = 20000)] + [SkipOnWslFact(Timeout = 20000)] public async Task GetDisplayNearestPoint_check() { var point = new Point @@ -65,7 +67,7 @@ public async Task GetDisplayNearestPoint_check() display.Size.Height.Should().BeGreaterThan(0); } - [Fact(Timeout = 20000)] + [SkipOnWslFact(Timeout = 20000)] public async Task GetDisplayMatching_check() { var rectangle = new Rectangle @@ -81,4 +83,4 @@ public async Task GetDisplayMatching_check() display.Size.Height.Should().BeGreaterThan(0); } } -} \ No newline at end of file +} diff --git a/src/ElectronNET.IntegrationTests/Tests/ShellTests.cs b/src/ElectronNET.IntegrationTests/Tests/ShellTests.cs index e81c762c..ff185db1 100644 --- a/src/ElectronNET.IntegrationTests/Tests/ShellTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/ShellTests.cs @@ -5,7 +5,7 @@ namespace ElectronNET.IntegrationTests.Tests [Collection("ElectronCollection")] public class ShellTests { - [Fact(Timeout = 20000)] + [Fact(Skip = "This can keep the test process hanging until the e-mail window is closed")] public async Task OpenExternal_invalid_scheme_returns_error_or_empty() { var error = await Electron.Shell.OpenExternalAsync("mailto:test@example.com"); diff --git a/src/ElectronNET.IntegrationTests/Tests/ThumbarButtonTests.cs b/src/ElectronNET.IntegrationTests/Tests/ThumbarButtonTests.cs index 2ba58b14..15253bd5 100644 --- a/src/ElectronNET.IntegrationTests/Tests/ThumbarButtonTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/ThumbarButtonTests.cs @@ -1,6 +1,6 @@ namespace ElectronNET.IntegrationTests.Tests { - using System.Runtime.InteropServices; + using System.Runtime.Versioning; using ElectronNET.API.Entities; [Collection("ElectronCollection")] @@ -13,7 +13,8 @@ public ThumbarButtonTests(ElectronFixture fx) this.fx = fx; } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("Windows")] public async Task SetThumbarButtons_returns_success() { var btn = new ThumbarButton("icon.png") { Tooltip = "Test" }; @@ -21,14 +22,10 @@ public async Task SetThumbarButtons_returns_success() success.Should().BeTrue(); } - [Fact(Timeout = 20000)] + [SkippableFact(Timeout = 20000)] + [SupportedOSPlatform("Windows")] public async Task Thumbar_button_click_invokes_callback() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return; // only meaningful on Windows taskbar - } - var icon = Path.Combine(Directory.GetCurrentDirectory(), "ElectronNET.WebApp", "wwwroot", "icon.png"); if (!File.Exists(icon)) { @@ -41,4 +38,4 @@ public async Task Thumbar_button_click_invokes_callback() ok.Should().BeTrue(); } } -} \ No newline at end of file +} diff --git a/src/ElectronNET.IntegrationTests/xunit.runner.json b/src/ElectronNET.IntegrationTests/xunit.runner.json new file mode 100644 index 00000000..9283731d --- /dev/null +++ b/src/ElectronNET.IntegrationTests/xunit.runner.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "methodDisplay": "method", + "diagnosticMessages": true, + "parallelizeTestCollections": false, + "longRunningTestSeconds": 60 +} diff --git a/src/ElectronNET/build/ElectronNET.Core.props b/src/ElectronNET/build/ElectronNET.Core.props index 0234fbb0..29d2c913 100644 --- a/src/ElectronNET/build/ElectronNET.Core.props +++ b/src/ElectronNET/build/ElectronNET.Core.props @@ -4,7 +4,7 @@ 30.4.0 26.0 - win-x64 + win-x64 true diff --git a/src/testEnvironments.json b/src/testEnvironments.json new file mode 100644 index 00000000..1208edc7 --- /dev/null +++ b/src/testEnvironments.json @@ -0,0 +1,17 @@ +{ + "version": "1", + "environments": [ + // See https://aka.ms/remotetesting for more details + // about how to configure remote environments. + { + "name": "WSL Ubuntu", + "type": "wsl", + "wslDistribution": "UbuFresh" + } + //{ + // "name": "Docker dotnet/sdk", + // "type": "docker", + // "dockerImage": "mcr.microsoft.com/dotnet/sdk" + //} + ] +} \ No newline at end of file