_collapsed = !_collapsed)"
+
_collapsed = !_collapsed)"
style="cursor: pointer; display: inline; font-weight: bold; color: white; padding: 5px; border-radius: 5px; margin-bottom: 5px;">@(_collapsed ? "\u25b6" : "\u25bc")
RunTests()">Run Class Tests
@if (_failed.Any())
@@ -18,13 +18,23 @@
@if (_running)
{
Running... @Remaining tests pending
-
,
+
+ if (_passed.Any() || _failed.Any())
+ {
+
|
+ }
}
- @if (_passed.Any() || _failed.Any())
+ @if (_passed.Any() || _failed.Any() || _inconclusive.Any())
{
-
Passed: @_passed.Count
-
,
-
Failed: @_failed.Count
+
Passed: @_passed.Count
+
|
+
Failed: @_failed.Count
+
+ if (_inconclusive.Any())
+ {
+
|
+
Inconclusive: @_inconclusive.Count
+ }
}
@@ -35,7 +45,10 @@
-
-@code {
-
- [Inject]
- public IJSRuntime JsRuntime { get; set; } = null!;
-
- [Inject]
- public JsModuleManager JsModuleManager { get; set; } = null!;
-
- [Inject]
- public NavigationManager NavigationManager { get; set; } = null!;
-
- [Parameter]
- public EventCallback
OnTestResults { get; set; }
-
- [Parameter]
- public TestResult? Results { get; set; }
-
- public async Task RunTests(bool onlyFailedTests = false, int skip = 0,
- CancellationToken cancellationToken = default)
- {
- _running = true;
-
- try
- {
- _resultBuilder = new StringBuilder();
- _passed.Clear();
-
- List methodsToRun = [];
-
- foreach (MethodInfo method in _methodInfos!.Skip(skip))
- {
- if (onlyFailedTests && !_failed.ContainsKey(method.Name))
- {
- continue;
- }
-
- _testResults[method.Name] = string.Empty;
- methodsToRun.Add(method);
- }
-
- _failed.Clear();
-
- foreach (MethodInfo method in methodsToRun)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- break;
- }
-
- await RunTest(method);
- }
-
- if (_retryTests.Any() && !cancellationToken.IsCancellationRequested)
- {
- await Task.Delay(1000, cancellationToken);
-
- foreach (MethodInfo retryMethod in _retryTests)
- {
- _failed.Remove(retryMethod.Name);
- await RunTest(retryMethod);
- }
- }
- }
- finally
- {
- _retryTests.Clear();
- _running = false;
- await OnTestResults.InvokeAsync(new TestResult(ClassName, _methodInfos!.Length, _passed, _failed, _running));
- StateHasChanged();
- }
- }
-
- public void Toggle(bool open)
- {
- _collapsed = !open;
- StateHasChanged();
- }
-
- protected override void OnInitialized()
- {
- _type = GetType();
- _methodInfos = _type
- .GetMethods()
- .Where(m => m.GetCustomAttribute(typeof(TestMethodAttribute), false) != null)
- .ToArray();
-
- _testResults = _methodInfos.ToDictionary(m => m.Name, _ => string.Empty);
- _interactionToggles = _methodInfos.ToDictionary(m => m.Name, _ => false);
- }
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- await base.OnAfterRenderAsync(firstRender);
-
- if (_jsObjectReference is null)
- {
- IJSObjectReference? proJs = await JsModuleManager.GetProJsModule(JsRuntime, default);
- IJSObjectReference coreJs = await JsModuleManager.GetCoreJsModule(JsRuntime, proJs, default);
-
- _jsObjectReference = await JsRuntime.InvokeAsync("import",
- "./_content/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/testRunner.js");
- await _jsObjectReference.InvokeVoidAsync("initialize", coreJs);
- }
-
- if (firstRender && Results is not null)
- {
- _passed = Results.Passed;
- _failed = Results.Failed;
- foreach (string passedTest in _passed.Keys)
- {
- _testResults[passedTest] = "Passed
";
- }
- foreach (string failedTest in _failed.Keys)
- {
- _testResults[failedTest] = "Failed
";
- }
-
- StateHasChanged();
- }
- }
-
- protected void AddMapRenderFragment(RenderFragment fragment, [CallerMemberName] string methodName = "")
- {
- _testRenderFragments[methodName] = fragment;
- }
-
- protected async Task WaitForMapToRender([CallerMemberName] string methodName = "", int timeoutInSeconds = 10)
- {
- //we are delaying by 100 milliseconds each try.
- //multiplying the timeout by 10 will get the correct number of tries
- var tries = timeoutInSeconds * 10;
-
- await InvokeAsync(StateHasChanged);
-
- while (!methodsWithRenderedMaps.Contains(methodName) && (tries > 0))
- {
- if (_mapRenderingExceptions.Remove(methodName, out Exception? ex))
- {
- ExceptionDispatchInfo.Capture(ex).Throw();
- }
-
- await Task.Delay(100);
- tries--;
- }
-
- if (!methodsWithRenderedMaps.Contains(methodName))
- {
- if (_running && _retryTests.All(mi => mi.Name != methodName))
- {
- // Sometimes running multiple tests causes timeouts, give this another chance.
- _retryTests.Add(_methodInfos!.First(mi => mi.Name == methodName));
- }
-
- throw new TimeoutException("Map did not render in allotted time.");
- }
-
- methodsWithRenderedMaps.Remove(methodName);
- }
-
- ///
- /// Handles the LayerViewCreated event and waits for a specific layer type to render.
- ///
- ///
- /// The name of the test method calling this function, used to track which layer view
- ///
- ///
- /// Optional timeout in seconds to wait for the layer to render. Defaults to 10 seconds.
- ///
- ///
- /// The type of layer to wait for rendering. Must inherit from .
- ///
- ///
- /// Returns the for the specified layer type once it has rendered.
- ///
- ///
- /// Throws if the specified layer type does not render within the allotted time.
- ///
- protected async Task WaitForLayerToRender(
- [CallerMemberName] string methodName = "",
- int timeoutInSeconds = 10) where TLayer: Layer
- {
- int tries = timeoutInSeconds * 10;
-
- while ((!layerViewCreatedEvents.ContainsKey(methodName)
- // check if the layer view was created for the specified layer type
- || layerViewCreatedEvents[methodName].All(lvce => lvce.Layer is not TLayer))
- && tries > 0)
- {
- await Task.Delay(100);
- tries--;
- }
-
- if (!layerViewCreatedEvents.ContainsKey(methodName)
- || layerViewCreatedEvents[methodName].All(lvce => lvce.Layer is not TLayer))
- {
- throw new TimeoutException($"Layer {typeof(TLayer).Name} did not render in allotted time, or LayerViewCreated was not set in MapView.OnLayerViewCreate");
- }
-
- LayerViewCreateEvent createEvent = layerViewCreatedEvents[methodName].First(lvce => lvce.Layer is TLayer);
- layerViewCreatedEvents[methodName].Remove(createEvent);
-
- return createEvent;
- }
-
- protected void ClearLayerViewEvents([CallerMemberName] string methodName = "")
- {
- layerViewCreatedEvents.Remove(methodName);
- }
-
- ///
- /// Handles the ListItemCreated event and waits for a ListItem to be created.
- ///
- ///
- /// The name of the test method calling this function, used to track which layer view
- ///
- ///
- /// Optional timeout in seconds to wait for the layer to render. Defaults to 10 seconds.
- ///
- ///
- /// Returns the .
- ///
- ///
- /// Throws if the specified layer type does not render within the allotted time.
- ///
- protected async Task WaitForListItemToBeCreated(
- [CallerMemberName] string methodName = "",
- int timeoutInSeconds = 10)
- {
- int tries = timeoutInSeconds * 10;
-
- while (!listItems.ContainsKey(methodName)
- && tries > 0)
- {
- await Task.Delay(100);
- tries--;
- }
-
- if (!listItems.TryGetValue(methodName, out List? items))
- {
- throw new TimeoutException("List Item did not render in allotted time, or ListItemCreated was not set in LayerListWidget.OnListItemCreatedHandler");
- }
-
- ListItem firstItem = items.First();
- listItems[methodName].Remove(firstItem);
-
- return firstItem;
- }
-
- protected async Task AssertJavaScript(string jsAssertFunction, [CallerMemberName] string methodName = "",
- int retryCount = 0, params object[] args)
- {
- try
- {
- List jsArgs = [methodName];
- jsArgs.AddRange(args);
-
- if (jsAssertFunction.Contains("."))
- {
- string[] parts = jsAssertFunction.Split('.');
-
- IJSObjectReference module = await JsRuntime.InvokeAsync("import",
- $"./_content/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/{parts[0]}.js");
- await module.InvokeVoidAsync(parts[1], jsArgs.ToArray());
- }
- else
- {
- await _jsObjectReference!.InvokeVoidAsync(jsAssertFunction, jsArgs.ToArray());
- }
- }
- catch (Exception)
- {
- if (retryCount < 4)
- {
- await Task.Delay(500);
- await AssertJavaScript(jsAssertFunction, methodName, retryCount + 1, args);
- }
- else
- {
- throw;
- }
- }
- }
-
- protected async Task WaitForJsTimeout(long time, [CallerMemberName] string methodName = "")
- {
- await _jsObjectReference!.InvokeVoidAsync("setJsTimeout", time, methodName);
-
- while (!await _jsObjectReference!.InvokeAsync("timeoutComplete", methodName))
- {
- await Task.Delay(100);
- }
- }
-
- private async Task RunTest(MethodInfo methodInfo)
- {
- _currentTest = methodInfo.Name;
- _testResults[methodInfo.Name] = "Running...
";
- _resultBuilder = new StringBuilder();
- _passed.Remove(methodInfo.Name);
- _failed.Remove(methodInfo.Name);
- _testRenderFragments.Remove(methodInfo.Name);
- _mapRenderingExceptions.Remove(methodInfo.Name);
- methodsWithRenderedMaps.Remove(methodInfo.Name);
- layerViewCreatedEvents.Remove(methodInfo.Name);
- listItems.Remove(methodInfo.Name);
- Console.WriteLine($"Running test {methodInfo.Name}");
-
- try
- {
- object[] actions = methodInfo.GetParameters()
- .Select(pi =>
- {
- Type paramType = pi.ParameterType;
-
- if (paramType == typeof(Action))
- {
- return (Action)(createEvent => LayerViewCreatedHandler(createEvent, methodInfo.Name));
- }
- if (paramType == typeof(Func>))
- {
- return (Func>)(item => ListItemCreatedHandler(item, methodInfo.Name));
- }
-
- return (Action)(() => RenderHandler(methodInfo.Name));
- })
- .ToArray();
-
- try
- {
- if (methodInfo.ReturnType == typeof(Task))
- {
- await (Task)methodInfo.Invoke(this, actions)!;
- }
- else
- {
- methodInfo.Invoke(this, actions);
- }
- }
- catch (TargetInvocationException tie) when (tie.InnerException is not null)
- {
- throw tie.InnerException;
- }
-
- _passed[methodInfo.Name] = _resultBuilder.ToString();
- _resultBuilder.AppendLine("Passed
");
- }
- catch (Exception ex)
- {
- if (_currentTest is null)
- {
- return;
- }
-
- if (!_retryTests.Contains(methodInfo))
- {
- _failed[methodInfo.Name] = $"{_resultBuilder}{Environment.NewLine}{ex.StackTrace}";
- _resultBuilder.AppendLine($"{ex.Message.Replace(Environment.NewLine, " ")} {ex.StackTrace?.Replace(Environment.NewLine, " ")}
");
- }
-
- if (ex.Message.Contains("Map component view is in an invalid state"))
- {
- await Task.Delay(1000);
- // force a full reload to recover from this error
- NavigationManager.NavigateTo("/", true);
- }
- }
-
- if (!_interactionToggles[methodInfo.Name])
- {
- await CleanupTest(methodInfo.Name);
- }
- }
-
- protected void Log(string message)
- {
- _resultBuilder.AppendLine($"{message}
");
- }
-
- [TestCleanup]
- protected async Task CleanupTest(string testName)
- {
- methodsWithRenderedMaps.Remove(testName);
- layerViewCreatedEvents.Remove(testName);
- _testResults[testName] = _resultBuilder.ToString();
- _testRenderFragments.Remove(testName);
-
- await InvokeAsync(async () =>
- {
- StateHasChanged();
- await OnTestResults.InvokeAsync(new TestResult(ClassName, _methodInfos!.Length, _passed, _failed, _running));
- });
- _interactionToggles[testName] = false;
- _currentTest = null;
- }
-
- private static void RenderHandler(string methodName)
- {
- methodsWithRenderedMaps.Add(methodName);
- }
-
- private static void LayerViewCreatedHandler(LayerViewCreateEvent createEvent, string methodName)
- {
- if (!layerViewCreatedEvents.ContainsKey(methodName))
- {
- layerViewCreatedEvents[methodName] = [];
- }
-
- layerViewCreatedEvents[methodName].Add(createEvent);
- }
-
- private static Task ListItemCreatedHandler(ListItem item, string methodName)
- {
- if (!listItems.ContainsKey(methodName))
- {
- listItems[methodName] = [];
- }
-
- listItems[methodName].Add(item);
-
- return Task.FromResult(item);
- }
-
- private void OnRenderError(ErrorEventArgs arg)
- {
- _mapRenderingExceptions[arg.MethodName] = arg.Exception;
- }
-
- private string ClassName => GetType().Name;
- private int Remaining => _methodInfos is null ? 0 : _methodInfos.Length - (_passed.Count + _failed.Count);
- private IJSObjectReference? _jsObjectReference;
- private StringBuilder _resultBuilder = new();
- private Type? _type;
- private MethodInfo[]? _methodInfos;
- private Dictionary _testResults = new();
- private bool _collapsed = true;
- private bool _running;
- private readonly Dictionary _testRenderFragments = new();
- private static readonly List methodsWithRenderedMaps = new();
- private static readonly Dictionary> layerViewCreatedEvents = new();
- private static readonly Dictionary> listItems = new();
- private readonly Dictionary _mapRenderingExceptions = new();
- private Dictionary _passed = new();
- private Dictionary _failed = new();
- private Dictionary _interactionToggles = [];
- private string? _currentTest;
- private readonly List _retryTests = [];
-}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestRunnerBase.razor.cs b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestRunnerBase.razor.cs
new file mode 100644
index 000000000..07507e71b
--- /dev/null
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestRunnerBase.razor.cs
@@ -0,0 +1,553 @@
+using dymaptic.GeoBlazor.Core.Components;
+using dymaptic.GeoBlazor.Core.Components.Layers;
+using dymaptic.GeoBlazor.Core.Events;
+using dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Logging;
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.ExceptionServices;
+using System.Text;
+using System.Text.RegularExpressions;
+
+
+namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Components;
+
+[TestClass]
+public partial class TestRunnerBase
+{
+ [Inject]
+ public required IJSRuntime JsRuntime { get; set; }
+ [Inject]
+ public required NavigationManager NavigationManager { get; set; }
+ [Inject]
+ public required JsModuleManager JsModuleManager { get; set; }
+ [Inject]
+ public required ITestLogger TestLogger { get; set; }
+ [Parameter]
+ public EventCallback
OnTestResults { get; set; }
+ [Parameter]
+ public TestResult? Results { get; set; }
+ [Parameter]
+ public IJSObjectReference? JsTestRunner { get; set; }
+
+ [CascadingParameter(Name = nameof(TestFilter))]
+ public string? TestFilter { get; set; }
+
+ private string? FilterValue => TestFilter?.Contains('.') == true ? TestFilter.Split('.')[1] : null;
+ private string ClassName => GetType().Name;
+ private int Remaining => _methodInfos is null
+ ? 0
+ : _methodInfos.Length - (_passed.Count + _failed.Count + _inconclusive.Count);
+
+ public async Task RunTests(bool onlyFailedTests = false, int skip = 0,
+ CancellationToken cancellationToken = default)
+ {
+ _running = true;
+
+ try
+ {
+ _resultBuilder = new StringBuilder();
+
+ if (!onlyFailedTests)
+ {
+ _passed.Clear();
+ _inconclusive.Clear();
+ }
+
+ List methodsToRun = [];
+ _filteredTestCount = 0;
+
+ foreach (MethodInfo method in _methodInfos!.Skip(skip))
+ {
+ if (onlyFailedTests
+ && (_passed.ContainsKey(method.Name) || _inconclusive.ContainsKey(method.Name)))
+ {
+ continue;
+ }
+
+ if (!FilterMatch(method.Name))
+ {
+ // skip filtered out test
+ continue;
+ }
+
+ _testResults[method.Name] = string.Empty;
+ methodsToRun.Add(method);
+ _filteredTestCount++;
+ }
+
+ _failed.Clear();
+
+ foreach (MethodInfo method in methodsToRun)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+
+ await RunTest(method);
+ }
+
+ for (int i = 1; i < 2; i++)
+ {
+ if (_retryTests.Any() && !cancellationToken.IsCancellationRequested)
+ {
+ List retryTests = _retryTests.ToList();
+ _retryTests.Clear();
+ _retry = i;
+ await Task.Delay(1000, cancellationToken);
+
+ foreach (MethodInfo retryMethod in retryTests)
+ {
+ await RunTest(retryMethod);
+ }
+ }
+ }
+ }
+ finally
+ {
+ _retryTests.Clear();
+ _running = false;
+ _retry = 0;
+
+ await OnTestResults.InvokeAsync(new TestResult(ClassName, _filteredTestCount, _passed, _failed,
+ _inconclusive, _running));
+ StateHasChanged();
+ }
+ }
+
+ public void Toggle(bool open)
+ {
+ _collapsed = !open;
+ StateHasChanged();
+ }
+
+ protected override void OnInitialized()
+ {
+ _type = GetType();
+
+ _methodInfos = _type
+ .GetMethods()
+ .Where(m => m.GetCustomAttribute(typeof(TestMethodAttribute), false) != null
+ && FilterMatch(m.Name))
+ .ToArray();
+
+ _testResults = _methodInfos
+ .ToDictionary(m => m.Name, _ => string.Empty);
+ _interactionToggles = _methodInfos.ToDictionary(m => m.Name, _ => false);
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+
+ if (firstRender && Results is not null)
+ {
+ _passed = Results.Passed;
+ _failed = Results.Failed;
+ _inconclusive = Results.Inconclusive;
+
+ foreach (string passedTest in _passed.Keys)
+ {
+ _testResults[passedTest] = "Passed
";
+ }
+
+ foreach (string failedTest in _failed.Keys)
+ {
+ _testResults[failedTest] = "Failed
";
+ }
+
+ foreach (string inconclusiveTest in _inconclusive.Keys)
+ {
+ _testResults[inconclusiveTest] = "Inconclusive
";
+ }
+
+ StateHasChanged();
+ }
+ }
+
+ protected void AddMapRenderFragment(RenderFragment fragment, [CallerMemberName] string methodName = "")
+ {
+ _testRenderFragments[methodName] = fragment;
+ }
+
+ protected async Task WaitForMapToRender([CallerMemberName] string methodName = "", int timeoutInSeconds = 10)
+ {
+ //we are delaying by 100 milliseconds each try.
+ //multiplying the timeout by 10 will get the correct number of tries
+ var tries = timeoutInSeconds * 10;
+
+ await InvokeAsync(StateHasChanged);
+
+ while (!methodsWithRenderedMaps.Contains(methodName) && (tries > 0))
+ {
+ if (_mapRenderingExceptions.Remove(methodName, out Exception? ex))
+ {
+ if (_running && _retry < 2 && _retryTests.All(mi => mi.Name != methodName)
+ && !ex.Message.Contains("Invalid GeoBlazor registration key")
+ && !ex.Message.Contains("Invalid GeoBlazor Pro license key")
+ && !ex.Message.Contains("No GeoBlazor Registration key provided")
+ && !ex.Message.Contains("No GeoBlazor Pro license key provided")
+ && !ex.Message.Contains("Map component view is in an invalid state"))
+ {
+ switch (_retry)
+ {
+ case 0:
+ _resultBuilder.AppendLine("First failure: will retry 2 more times");
+
+ break;
+ case 1:
+ _resultBuilder.AppendLine("Second failure: will retry 1 more times");
+
+ break;
+ }
+
+ // Sometimes running multiple tests causes timeouts, give this another chance.
+ _retryTests.Add(_methodInfos!.First(mi => mi.Name == methodName));
+ }
+
+ await TestLogger.LogError("Test Failed", ex);
+
+ ExceptionDispatchInfo.Capture(ex).Throw();
+ }
+
+ await Task.Delay(100);
+ tries--;
+ }
+
+ if (!methodsWithRenderedMaps.Contains(methodName))
+ {
+ if (_running && _retryTests.All(mi => mi.Name != methodName))
+ {
+ // Sometimes running multiple tests causes timeouts, give this another chance.
+ _retryTests.Add(_methodInfos!.First(mi => mi.Name == methodName));
+
+ throw new TimeoutException("Map did not render in allotted time. Will re-attempt shortly...");
+ }
+
+ throw new TimeoutException("Map did not render in allotted time.");
+ }
+
+ methodsWithRenderedMaps.Remove(methodName);
+ }
+
+ ///
+ /// Handles the LayerViewCreated event and waits for a specific layer type to render.
+ ///
+ ///
+ /// The name of the test method calling this function, used to track which layer view
+ ///
+ ///
+ /// Optional timeout in seconds to wait for the layer to render. Defaults to 10 seconds.
+ ///
+ ///
+ /// The type of layer to wait for rendering. Must inherit from .
+ ///
+ ///
+ /// Returns the for the specified layer type once it has rendered.
+ ///
+ ///
+ /// Throws if the specified layer type does not render within the allotted time.
+ ///
+ protected async Task WaitForLayerToRender([CallerMemberName] string methodName = "",
+ int timeoutInSeconds = 10) where TLayer : Layer
+ {
+ int tries = timeoutInSeconds * 10;
+
+ while ((!layerViewCreatedEvents.ContainsKey(methodName)
+
+ // check if the layer view was created for the specified layer type
+ || layerViewCreatedEvents[methodName].All(lvce => lvce.Layer is not TLayer))
+ && tries > 0)
+ {
+ await Task.Delay(100);
+ tries--;
+ }
+
+ if (!layerViewCreatedEvents.ContainsKey(methodName)
+ || layerViewCreatedEvents[methodName].All(lvce => lvce.Layer is not TLayer))
+ {
+ throw new TimeoutException($"Layer {typeof(TLayer).Name
+ } did not render in allotted time, or LayerViewCreated was not set in MapView.OnLayerViewCreate");
+ }
+
+ LayerViewCreateEvent createEvent = layerViewCreatedEvents[methodName].First(lvce => lvce.Layer is TLayer);
+ layerViewCreatedEvents[methodName].Remove(createEvent);
+
+ return createEvent;
+ }
+
+ protected void ClearLayerViewEvents([CallerMemberName] string methodName = "")
+ {
+ layerViewCreatedEvents.Remove(methodName);
+ }
+
+ ///
+ /// Handles the ListItemCreated event and waits for a ListItem to be created.
+ ///
+ ///
+ /// The name of the test method calling this function, used to track which layer view
+ ///
+ ///
+ /// Optional timeout in seconds to wait for the layer to render. Defaults to 10 seconds.
+ ///
+ ///
+ /// Returns the .
+ ///
+ ///
+ /// Throws if the specified layer type does not render within the allotted time.
+ ///
+ protected async Task WaitForListItemToBeCreated([CallerMemberName] string methodName = "",
+ int timeoutInSeconds = 10)
+ {
+ int tries = timeoutInSeconds * 10;
+
+ while (!listItems.ContainsKey(methodName)
+ && tries > 0)
+ {
+ await Task.Delay(100);
+ tries--;
+ }
+
+ if (!listItems.TryGetValue(methodName, out List? items))
+ {
+ throw new TimeoutException(
+ "List Item did not render in allotted time, or ListItemCreated was not set in LayerListWidget.OnListItemCreatedHandler");
+ }
+
+ ListItem firstItem = items.First();
+ listItems[methodName].Remove(firstItem);
+
+ return firstItem;
+ }
+
+ protected async Task AssertJavaScript(string jsAssertFunction, [CallerMemberName] string methodName = "",
+ int retryCount = 0, params object[] args)
+ {
+ try
+ {
+ List jsArgs = [methodName];
+ jsArgs.AddRange(args);
+
+ if (jsAssertFunction.Contains("."))
+ {
+ string[] parts = jsAssertFunction.Split('.');
+
+ IJSObjectReference module = await JsRuntime.InvokeAsync("import",
+ $"./_content/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/{parts[0]}.js");
+ await module.InvokeVoidAsync(parts[1], jsArgs.ToArray());
+ }
+ else
+ {
+ await JsTestRunner!.InvokeVoidAsync(jsAssertFunction, jsArgs.ToArray());
+ }
+ }
+ catch (Exception)
+ {
+ if (retryCount < 4)
+ {
+ await Task.Delay(500);
+ await AssertJavaScript(jsAssertFunction, methodName, retryCount + 1, args);
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+
+ protected async Task WaitForJsTimeout(long time, [CallerMemberName] string methodName = "")
+ {
+ await JsTestRunner!.InvokeVoidAsync("setJsTimeout", time, methodName);
+
+ while (!await JsTestRunner!.InvokeAsync("timeoutComplete", methodName))
+ {
+ await Task.Delay(100);
+ }
+ }
+
+ protected void Log(string message)
+ {
+ _resultBuilder.AppendLine($"{message}
");
+ }
+
+ protected async Task CleanupTest(string testName)
+ {
+ methodsWithRenderedMaps.Remove(testName);
+ layerViewCreatedEvents.Remove(testName);
+ _testResults[testName] = _resultBuilder.ToString();
+ _testRenderFragments.Remove(testName);
+
+ await InvokeAsync(async () =>
+ {
+ StateHasChanged();
+
+ await OnTestResults.InvokeAsync(new TestResult(ClassName, _filteredTestCount, _passed, _failed,
+ _inconclusive, _running));
+ });
+ _interactionToggles[testName] = false;
+ _currentTest = null;
+ }
+
+ private static void RenderHandler(string methodName)
+ {
+ methodsWithRenderedMaps.Add(methodName);
+ }
+
+ private static void LayerViewCreatedHandler(LayerViewCreateEvent createEvent, string methodName)
+ {
+ if (!layerViewCreatedEvents.ContainsKey(methodName))
+ {
+ layerViewCreatedEvents[methodName] = [];
+ }
+
+ layerViewCreatedEvents[methodName].Add(createEvent);
+ }
+
+ private static Task ListItemCreatedHandler(ListItem item, string methodName)
+ {
+ if (!listItems.ContainsKey(methodName))
+ {
+ listItems[methodName] = [];
+ }
+
+ listItems[methodName].Add(item);
+
+ return Task.FromResult(item);
+ }
+
+ private async Task RunTest(MethodInfo methodInfo)
+ {
+ if (JsTestRunner is null)
+ {
+ await GetJsTestRunner();
+ }
+
+ _currentTest = methodInfo.Name;
+ _testResults[methodInfo.Name] = "Running...
";
+ _resultBuilder = new StringBuilder();
+ _passed.Remove(methodInfo.Name);
+ _failed.Remove(methodInfo.Name);
+ _inconclusive.Remove(methodInfo.Name);
+ _testRenderFragments.Remove(methodInfo.Name);
+ _mapRenderingExceptions.Remove(methodInfo.Name);
+ methodsWithRenderedMaps.Remove(methodInfo.Name);
+ layerViewCreatedEvents.Remove(methodInfo.Name);
+ listItems.Remove(methodInfo.Name);
+ await TestLogger.Log($"Running test {methodInfo.Name}");
+
+ try
+ {
+ var actions = methodInfo.GetParameters()
+ .Select(pi =>
+ {
+ var paramType = pi.ParameterType;
+
+ if (paramType == typeof(Action))
+ {
+ return (Action)(createEvent =>
+ LayerViewCreatedHandler(createEvent, methodInfo.Name));
+ }
+
+ if (paramType == typeof(Func>))
+ {
+ return (Func>)(item => ListItemCreatedHandler(item, methodInfo.Name));
+ }
+
+ return (Action)(() => RenderHandler(methodInfo.Name));
+ })
+ .ToArray();
+
+ try
+ {
+ if (methodInfo.ReturnType == typeof(Task))
+ {
+ await (Task)methodInfo.Invoke(this, actions)!;
+ }
+ else
+ {
+ methodInfo.Invoke(this, actions);
+ }
+ }
+ catch (TargetInvocationException tie) when (tie.InnerException is not null)
+ {
+ throw tie.InnerException;
+ }
+
+ _passed[methodInfo.Name] = _resultBuilder.ToString();
+ _resultBuilder.AppendLine("Passed
");
+ }
+ catch (Exception ex)
+ {
+ if (_currentTest is null)
+ {
+ return;
+ }
+
+ var textResult = $"{_resultBuilder}{Environment.NewLine}{ex.Message}{Environment.NewLine}{ex.StackTrace
+ }";
+ string displayColor;
+
+ if (ex is AssertInconclusiveException)
+ {
+ _inconclusive[methodInfo.Name] = textResult;
+ displayColor = "white";
+ }
+ else
+ {
+ _failed[methodInfo.Name] = textResult;
+ displayColor = "red";
+ }
+
+ _resultBuilder.AppendLine($"{
+ ex.Message.Replace(Environment.NewLine, " ")} {
+ ex.StackTrace?.Replace(Environment.NewLine, " ")}
");
+ }
+
+ if (!_interactionToggles[methodInfo.Name])
+ {
+ await CleanupTest(methodInfo.Name);
+ }
+ }
+
+ private void OnRenderError(ErrorEventArgs arg)
+ {
+ _mapRenderingExceptions[arg.MethodName] = arg.Exception;
+ }
+
+ private async Task GetJsTestRunner()
+ {
+ JsTestRunner = await JsRuntime.InvokeAsync("import",
+ "./_content/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/testRunner.js");
+ IJSObjectReference? proJs = await JsModuleManager.GetProJsModule(JsRuntime, CancellationToken.None);
+ IJSObjectReference coreJs = await JsModuleManager.GetCoreJsModule(JsRuntime, proJs, CancellationToken.None);
+ await JsTestRunner.InvokeVoidAsync("initialize", coreJs);
+ }
+
+ private bool FilterMatch(string testName)
+ {
+ return FilterValue is null
+ || Regex.IsMatch(testName, $"^{FilterValue}$", RegexOptions.IgnoreCase);
+ }
+
+ private static readonly List methodsWithRenderedMaps = new();
+ private static readonly Dictionary> layerViewCreatedEvents = new();
+ private static readonly Dictionary> listItems = new();
+ private readonly Dictionary _testRenderFragments = new();
+ private readonly Dictionary _mapRenderingExceptions = new();
+ private readonly List _retryTests = [];
+ private StringBuilder _resultBuilder = new();
+ private Type? _type;
+ private MethodInfo[]? _methodInfos;
+ private Dictionary _testResults = new();
+ private bool _collapsed = true;
+ private bool _running;
+ private Dictionary _passed = new();
+ private Dictionary _failed = new();
+ private Dictionary _inconclusive = new();
+ private int _filteredTestCount;
+ private Dictionary _interactionToggles = [];
+ private string? _currentTest;
+ private int _retry;
+}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestWrapper.razor b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestWrapper.razor
index c3a3be6d5..5268007ff 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestWrapper.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/TestWrapper.razor
@@ -37,6 +37,10 @@
[Parameter]
[EditorRequired]
public TestResult? Results { get; set; }
+
+ [Parameter]
+ [EditorRequired]
+ public required IJSObjectReference JsTestRunner { get; set; }
public async Task RunTests(bool onlyFailedTests = false, int skip = 0, CancellationToken cancellationToken = default)
{
@@ -113,7 +117,8 @@
private Dictionary Parameters => new()
{
{ nameof(OnTestResults), OnTestResults },
- { nameof(Results), Results }
+ { nameof(Results), Results },
+ { nameof(JsTestRunner), JsTestRunner }
};
private BlazorFrame.BlazorFrame? _isolatedFrame;
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WMSLayerTests.razor b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WMSLayerTests.razor
index 256c112fa..65f256746 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WMSLayerTests.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WMSLayerTests.razor
@@ -27,7 +27,7 @@
);
- await WaitForMapToRender();
+ await WaitForMapToRender(timeoutInSeconds: 30);
LayerViewCreateEvent createEvent = await WaitForLayerToRender();
Assert.IsInstanceOfType(createEvent.Layer);
@@ -56,7 +56,7 @@
);
- await WaitForMapToRender();
+ await WaitForMapToRender(timeoutInSeconds: 30);
LayerViewCreateEvent createEvent = await WaitForLayerToRender();
Assert.IsInstanceOfType(createEvent.Layer);
WMSLayer createdLayer = (WMSLayer)createEvent.Layer;
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor
index 0f3885d3e..668fbdd9f 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor
@@ -140,7 +140,7 @@
OnClick="ClickHandler">
-
+
);
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Configuration/ConfigurationHelper.cs b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Configuration/ConfigurationHelper.cs
new file mode 100644
index 000000000..71d73a0d1
--- /dev/null
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Configuration/ConfigurationHelper.cs
@@ -0,0 +1,45 @@
+using Microsoft.Extensions.Configuration;
+using System.Text.Json;
+
+
+namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Configuration;
+
+public static class ConfigurationHelper
+{
+
+ ///
+ /// Recursively converts IConfiguration to a nested Dictionary and serializes to JSON.
+ ///
+ public static string ToJson(this IConfiguration config)
+ {
+ var dict = ToDictionary(config);
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true // Pretty print
+ };
+ return JsonSerializer.Serialize(dict, options);
+ }
+
+ ///
+ /// Recursively builds a dictionary from IConfiguration.
+ ///
+ private static Dictionary ToDictionary(IConfiguration config)
+ {
+ var result = new Dictionary();
+
+ foreach (var child in config.GetChildren())
+ {
+ // If the child has further children, recurse
+ if (child.GetChildren().Any())
+ {
+ result[child.Key] = ToDictionary(child);
+ }
+ else
+ {
+ result[child.Key] = child.Value;
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Logging/ITestLogger.cs b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Logging/ITestLogger.cs
new file mode 100644
index 000000000..84cadcc7b
--- /dev/null
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Logging/ITestLogger.cs
@@ -0,0 +1,133 @@
+using Microsoft.Extensions.Logging;
+using System.Net.Http.Json;
+using System.Text;
+
+
+namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Logging;
+
+public interface ITestLogger
+{
+ public Task Log(string message);
+
+ public Task LogError(string message, Exception? exception = null);
+
+ public Task LogError(string message, SerializableException? exception);
+}
+
+public class ServerTestLogger(ILogger logger) : ITestLogger
+{
+ public Task Log(string message)
+ {
+ logger.LogInformation(message);
+
+ return Task.CompletedTask;
+ }
+
+ public Task LogError(string message, Exception? exception = null)
+ {
+ logger.LogError(exception, message);
+ return Task.CompletedTask;
+ }
+
+ public Task LogError(string message, SerializableException? exception)
+ {
+ if (exception is not null)
+ {
+ logger.LogError("{Message}\n{Exception}", message, exception.ToString());
+ }
+ else
+ {
+ logger.LogError("{Message}", message);
+ }
+
+ return Task.CompletedTask;
+ }
+}
+
+public class ClientTestLogger(IHttpClientFactory httpClientFactory, ILogger logger) : ITestLogger
+{
+ public async Task Log(string message)
+ {
+ using var httpClient = httpClientFactory.CreateClient(nameof(ClientTestLogger));
+ logger.LogInformation(message);
+
+ try
+ {
+ await httpClient.PostAsJsonAsync("/log", new LogMessage(message, null));
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error sending log message to server");
+ }
+ }
+
+ public async Task LogError(string message, Exception? exception = null)
+ {
+ await LogError(message, SerializableException.FromException(exception));
+ }
+
+ public async Task LogError(string message, SerializableException? exception)
+ {
+ using var httpClient = httpClientFactory.CreateClient(nameof(ClientTestLogger));
+
+ if (exception is not null)
+ {
+ logger.LogError("{Message}\n{Exception}", message, exception.ToString());
+ }
+ else
+ {
+ logger.LogError("{Message}", message);
+ }
+
+ try
+ {
+ await httpClient.PostAsJsonAsync("/log-error", new LogMessage(message, exception));
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error sending log message to server");
+ }
+ }
+}
+
+public record LogMessage(string Message, SerializableException? Exception);
+
+///
+/// A serializable representation of an exception that preserves all important information
+/// including the stack trace, which is lost when deserializing a regular Exception.
+///
+public record SerializableException(
+ string Type,
+ string Message,
+ string? StackTrace,
+ SerializableException? InnerException)
+{
+ public static SerializableException? FromException(Exception? exception)
+ {
+ if (exception is null) return null;
+
+ return new SerializableException(
+ exception.GetType().FullName ?? exception.GetType().Name,
+ exception.Message,
+ exception.StackTrace,
+ FromException(exception.InnerException));
+ }
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"{Type}: {Message}");
+
+ if (!string.IsNullOrEmpty(StackTrace))
+ {
+ sb.AppendLine(StackTrace);
+ }
+
+ if (InnerException is not null)
+ {
+ sb.AppendLine($" ---> {InnerException}");
+ }
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor
index baf366c17..d0083bda9 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor
@@ -30,7 +30,7 @@ else
if (_running)
{
-
@if (_running)
{
-
Running... @Remaining tests pending
+
Running... @Remaining tests pending
}
else if (_results.Any())
{
-
Complete
-
,
-
Passed: @Passed
-
,
-
Failed: @Failed
+
Complete
+
|
+
Passed: @Passed
+
|
+
Failed: @Failed
+
+ if (Inconclusive > 0)
+ {
+
|
+
Inconclusive: @Inconclusive
+ }
}
@foreach (KeyValuePair
result in _results.OrderBy(kvp => kvp.Key))
{
ScrollAndOpenClass(result.Key))">
- @Extensions.CamelCaseToSpaces(result.Key) - @((MarkupString)$"Passed: {result.Value.Passed.Count} , Failed: {result.Value.Failed.Count} ")
+ @BuildResultSummaryLine(result.Key, result.Value)
}
@@ -67,264 +73,13 @@ else
foreach (Type type in _testClassTypes)
{
- bool isIsolated = type.GetCustomAttribute() != null;
+ bool isIsolated = _testClassTypes.Count > 1
+ && type.GetCustomAttribute() != null;
}
-}
-
-@code {
- [Inject]
- public required IConfiguration Configuration { get; set; }
-
- [Inject]
- public required IHostApplicationLifetime HostApplicationLifetime { get; set; }
-
- [Inject]
- public required IJSRuntime JsRuntime { get; set; }
-
- [Inject]
- public required NavigationManager NavigationManager { get; set; }
-
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- _jsObjectReference ??= await JsRuntime.InvokeAsync("import", "./_content/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/testRunner.js");
- if (firstRender)
- {
- NavigationManager.RegisterLocationChangingHandler(OnLocationChanging);
-
- await LoadSettings();
-
- StateHasChanged();
-
- if (!_settings.RetainResultsOnReload)
- {
- return;
- }
-
- FindAllTests();
-
- Dictionary? cachedResults =
- await _jsObjectReference.InvokeAsync?>("getTestResults");
-
- if (cachedResults is { Count: > 0 })
- {
- _results = cachedResults;
- }
-
- if (Configuration["runOnStart"] == "true")
- {
- bool passed = await RunTests(false, _cts.Token);
-
- if (!passed)
- {
- Environment.ExitCode = 1;
- }
- HostApplicationLifetime.StopApplication();
- }
-
- if (_results!.Count > 0)
- {
- string? firstUnpassedClass = _testClassNames
- .FirstOrDefault(t => !_results.ContainsKey(t) || _results[t].Passed.Count == 0);
- if (firstUnpassedClass is not null && _testClassNames.IndexOf(firstUnpassedClass) > 0)
- {
- await ScrollAndOpenClass(firstUnpassedClass);
- }
- }
- StateHasChanged();
- }
- }
-
- private void FindAllTests()
- {
- _results = [];
- var assembly = Assembly.GetExecutingAssembly();
- Type[] types = assembly.GetTypes();
- try
- {
- var proAssembly = Assembly.Load("dymaptic.GeoBlazor.Pro.Test.Blazor.Shared");
- types = types.Concat(proAssembly.GetTypes()
- .Where(t => t.Name != "ProTestRunnerBase")).ToArray();
- }
- catch
- {
- //ignore if not running pro
- }
- foreach (Type type in types.Where(t => !t.Name.EndsWith("GeneratedTests")))
- {
- if (type.IsAssignableTo(typeof(TestRunnerBase)) && (type.Name != nameof(TestRunnerBase)))
- {
- _testClassTypes.Add(type);
- _testComponents[type.Name] = null;
-
- int testCount = type.GetMethods()
- .Count(m => m.GetCustomAttribute(typeof(TestMethodAttribute), false) != null);
- _results![type.Name] = new TestResult(type.Name, testCount, [], [], false);
- }
- }
-
- // sort alphabetically
- _testClassTypes.Sort((t1, t2) => string.Compare(t1.Name, t2.Name, StringComparison.Ordinal));
- _testClassNames = _testClassTypes.Select(t => t.Name).ToList();
- }
-
- private async Task RunNewTests(bool onlyFailedTests = false, CancellationToken token = default)
- {
- string? firstUntestedClass = _testClassNames
- .FirstOrDefault(t => !_results!.ContainsKey(t) || _results[t].Passed.Count == 0);
-
- if (firstUntestedClass is not null)
- {
- int index = _testClassNames.IndexOf(firstUntestedClass);
- await RunTests(onlyFailedTests, token, index);
- }
- else
- {
- await RunTests(onlyFailedTests, token);
- }
- }
-
- private async Task RunTests(bool onlyFailedTests = false, CancellationToken token = default,
- int offset = 0)
- {
- _running = true;
- foreach (var kvp in _testComponents.OrderBy(k => _testClassNames.IndexOf(k.Key)).Skip(offset))
- {
- if (token.IsCancellationRequested)
- {
- break;
- }
-
- if (_results!.TryGetValue(kvp.Key, out TestResult? results))
- {
- if (onlyFailedTests && results.Failed.Count == 0)
- {
- break;
- }
- }
- if (kvp.Value != null)
- {
- await kvp.Value!.RunTests(onlyFailedTests, cancellationToken: token);
- }
- }
-
- _running = false;
- await InvokeAsync(StateHasChanged);
- var resultBuilder = new StringBuilder($@"
-# GeoBlazor Unit Test Results
-{DateTime.Now}
-Passed: {_results!.Values.Select(r => r.Passed.Count).Sum()}
-Failed: {_results.Values.Select(r => r.Failed.Count).Sum()}");
- foreach (KeyValuePair result in _results)
- {
- resultBuilder.AppendLine($@"
-## {result.Key}
-Passed: {result.Value.Passed.Count}
-Failed: {result.Value.Failed.Count}");
- foreach (KeyValuePair methodResult in result.Value.Passed)
- {
- resultBuilder.AppendLine($@"### {methodResult.Key} - Passed
-{methodResult.Value}");
- }
-
- foreach (KeyValuePair methodResult in result.Value.Failed)
- {
- resultBuilder.AppendLine($@"### {methodResult.Key} - Failed
-{methodResult.Value}");
- }
- }
- Console.WriteLine(resultBuilder.ToString());
-
- return _results.Values.All(r => r.Failed.Count == 0);
- }
-
- private async Task OnTestResults(TestResult result)
- {
- _results![result.ClassName] = result;
- await SaveResults();
- await InvokeAsync(StateHasChanged);
- if (_settings.StopOnFail && result.Failed.Count > 0)
- {
- await CancelRun();
- await ScrollAndOpenClass(result.ClassName);
- }
- }
-
- private void ToggleAll()
- {
- _showAll = !_showAll;
- foreach (TestWrapper? component in _testComponents.Values)
- {
- component?.Toggle(_showAll);
- }
- }
-
- private async Task ScrollAndOpenClass(string className)
- {
- await _jsObjectReference!.InvokeVoidAsync("scrollToTestClass", className);
- TestWrapper? testClass = _testComponents[className];
- testClass?.Toggle(true);
- }
-
- private async Task CancelRun()
- {
- await _jsObjectReference!.InvokeVoidAsync("setWaitCursor", false);
- await Task.Yield();
-
- await InvokeAsync(async () =>
- {
- await _cts.CancelAsync();
- _cts = new CancellationTokenSource();
- });
- }
-
- private async ValueTask OnLocationChanging(LocationChangingContext context)
- {
- await SaveResults();
- }
-
- private async Task SaveResults()
- {
- await _jsObjectReference!.InvokeVoidAsync("saveTestResults", _results);
- }
-
- private async Task SaveSettings()
- {
- await _jsObjectReference!.InvokeVoidAsync("saveSettings", _settings);
- }
-
- private async Task LoadSettings()
- {
- TestSettings? settings = await _jsObjectReference!.InvokeAsync("loadSettings");
- if (settings is not null)
- {
- _settings = settings;
- }
- }
-
- private int Remaining => _results?.Sum(r =>
- r.Value.TestCount - (r.Value.Passed.Count + r.Value.Failed.Count)) ?? 0;
- private int Passed => _results?.Sum(r => r.Value.Passed.Count) ?? 0;
- private int Failed => _results?.Sum(r => r.Value.Failed.Count) ?? 0;
- private IJSObjectReference? _jsObjectReference;
- private Dictionary? _results;
- private bool _running;
- private readonly List _testClassTypes = [];
- private List _testClassNames = [];
- private readonly Dictionary _testComponents = new();
- private bool _showAll;
- private CancellationTokenSource _cts = new();
- private TestSettings _settings = new(true, true);
-
- public record TestSettings(bool StopOnFail, bool RetainResultsOnReload)
- {
- public bool StopOnFail { get; set; } = StopOnFail;
- public bool RetainResultsOnReload { get; set; } = RetainResultsOnReload;
- }
-
}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor.cs b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor.cs
new file mode 100644
index 000000000..d8756f55c
--- /dev/null
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/Index.razor.cs
@@ -0,0 +1,411 @@
+using dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Components;
+using dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Logging;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Routing;
+using Microsoft.Extensions.Configuration;
+using Microsoft.JSInterop;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+
+
+namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Pages;
+
+public partial class Index
+{
+ [Inject]
+ public required IJSRuntime JsRuntime { get; set; }
+ [Inject]
+ public required NavigationManager NavigationManager { get; set; }
+ [Inject]
+ public required JsModuleManager JsModuleManager { get; set; }
+ [Inject]
+ public required ITestLogger TestLogger { get; set; }
+ [Inject]
+ public required IAppValidator AppValidator { get; set; }
+ [Inject]
+ public required IConfiguration Configuration { get; set; }
+ [CascadingParameter(Name = nameof(RunOnStart))]
+ public required bool RunOnStart { get; set; }
+ ///
+ /// Only run Pro Tests
+ ///
+ [CascadingParameter(Name = nameof(ProOnly))]
+ public required bool ProOnly { get; set; }
+
+ [CascadingParameter(Name = nameof(TestFilter))]
+ public string? TestFilter { get; set; }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (_allPassed)
+ {
+ return;
+ }
+
+ if (firstRender)
+ {
+ try
+ {
+ await AppValidator.ValidateLicense();
+ }
+ catch (Exception)
+ {
+ IConfigurationSection geoblazorConfig = Configuration.GetSection("GeoBlazor");
+
+ throw new InvalidRegistrationException($"Failed to validate GeoBlazor License Key: {
+ geoblazorConfig.GetValue("LicenseKey", geoblazorConfig.GetValue("RegistrationKey", "No Key Found"))
+ }");
+ }
+
+ _jsTestRunner = await JsRuntime.InvokeAsync("import",
+ "./_content/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/testRunner.js");
+ IJSObjectReference? proJs = await JsModuleManager.GetProJsModule(JsRuntime, CancellationToken.None);
+ IJSObjectReference coreJs = await JsModuleManager.GetCoreJsModule(JsRuntime, proJs, CancellationToken.None);
+ WFSServer[] wfsServers = Configuration.GetSection("WFSServers").Get()!;
+ await _jsTestRunner.InvokeVoidAsync("initialize", coreJs, wfsServers);
+
+ NavigationManager.RegisterLocationChangingHandler(OnLocationChanging);
+
+ await LoadSettings();
+
+ if (!_settings.RetainResultsOnReload)
+ {
+ return;
+ }
+
+ FindAllTests();
+
+ Dictionary? cachedResults =
+ await _jsTestRunner.InvokeAsync?>("getTestResults");
+
+ if (cachedResults is { Count: > 0 })
+ {
+ _results = cachedResults;
+ }
+
+ if (_results!.Count > 0)
+ {
+ string? firstUnpassedClass = _testClassNames
+ .FirstOrDefault(t => !_results.ContainsKey(t)
+ || (_results[t].Passed.Count == 0 && _results[t].Inconclusive.Count == 0));
+
+ if (firstUnpassedClass is not null && _testClassNames.IndexOf(firstUnpassedClass) > 0)
+ {
+ await ScrollAndOpenClass(firstUnpassedClass);
+ }
+ }
+
+ // need an extra render cycle to register the `_testComponents` dictionary
+ StateHasChanged();
+ }
+ else if (RunOnStart && !_running)
+ {
+ // Auto-run configuration
+ _running = true;
+
+ // give everything time to load correctly
+ await Task.Delay(1000);
+ await TestLogger.Log("Starting Test Auto-Run:");
+ string? attempts = await JsRuntime.InvokeAsync("localStorage.getItem", "runAttempts");
+
+ int attemptCount = 0;
+
+ if (attempts is not null && int.TryParse(attempts, out attemptCount))
+ {
+ await TestLogger.Log($"Attempt #{attemptCount}");
+ }
+
+ await TestLogger.Log("----------");
+
+ _allPassed = await RunTests(true, _cts.Token);
+
+ if (!_allPassed)
+ {
+ await TestLogger.Log(
+ "Test Run Failed or Errors Encountered. Reload the page to re-run failed tests.");
+ await JsRuntime.InvokeVoidAsync("localStorage.setItem", "runAttempts", ++attemptCount);
+ }
+ }
+ }
+
+ private void FindAllTests()
+ {
+ _results = [];
+ Type[] types;
+
+ if (ProOnly)
+ {
+ var proAssembly = Assembly.Load("dymaptic.GeoBlazor.Pro.Test.Blazor.Shared");
+
+ types = proAssembly.GetTypes()
+ .Where(t => t.Name != "ProTestRunnerBase")
+ .ToArray();
+ }
+ else
+ {
+ var assembly = Assembly.Load("dymaptic.GeoBlazor.Core.Test.Blazor.Shared");
+ types = assembly.GetTypes();
+
+ try
+ {
+ var proAssembly = Assembly.Load("dymaptic.GeoBlazor.Pro.Test.Blazor.Shared");
+
+ types = types.Concat(proAssembly.GetTypes()
+ .Where(t => t.Name != "ProTestRunnerBase"))
+ .ToArray();
+ }
+ catch
+ {
+ //ignore if not running pro
+ }
+ }
+
+ foreach (Type type in types)
+ {
+ if (!string.IsNullOrWhiteSpace(TestFilter))
+ {
+ string filter = TestFilter.Split('.')[0];
+ if (!Regex.IsMatch(type.Name, $"^{filter}$", RegexOptions.IgnoreCase))
+ {
+ continue;
+ }
+ }
+
+ if (type.IsAssignableTo(typeof(TestRunnerBase)) && (type.Name != nameof(TestRunnerBase)))
+ {
+ _testClassTypes.Add(type);
+ _testComponents[type.Name] = null;
+
+ int testCount = type.GetMethods()
+ .Count(m => m.GetCustomAttribute(typeof(TestMethodAttribute), false) != null);
+ _results![type.Name] = new TestResult(type.Name, testCount, [], [], [], false);
+ }
+ }
+
+ // sort alphabetically
+ _testClassTypes.Sort((t1, t2) => string.Compare(t1.Name, t2.Name, StringComparison.Ordinal));
+ _testClassNames = _testClassTypes.Select(t => t.Name).ToList();
+ }
+
+ private async Task RunNewTests(bool onlyFailedTests = false, CancellationToken token = default)
+ {
+ string? firstUntestedClass = _testClassNames
+ .FirstOrDefault(t => !_results!.ContainsKey(t)
+ || (_results[t].Passed.Count == 0 && _results[t].Inconclusive.Count == 0));
+
+ if (firstUntestedClass is not null)
+ {
+ int index = _testClassNames.IndexOf(firstUntestedClass);
+ await RunTests(onlyFailedTests, token, index);
+ }
+ else
+ {
+ await RunTests(onlyFailedTests, token);
+ }
+ }
+
+ private async Task RunTests(bool onlyFailedTests = false, CancellationToken token = default,
+ int offset = 0)
+ {
+ _running = true;
+
+ foreach (var kvp in _testComponents.OrderBy(k => _testClassNames.IndexOf(k.Key)).Skip(offset))
+ {
+ if (token.IsCancellationRequested)
+ {
+ break;
+ }
+
+ if (_results!.TryGetValue(kvp.Key, out TestResult? results))
+ {
+ if (onlyFailedTests && results.Failed.Count == 0
+ && (results.Passed.Count > 0 || results.Inconclusive.Count > 0))
+ {
+ break;
+ }
+ }
+
+ if (kvp.Value != null)
+ {
+ await kvp.Value!.RunTests(onlyFailedTests, cancellationToken: token);
+ }
+ }
+
+ var resultBuilder = new StringBuilder($"""
+
+ # GeoBlazor Unit Test Results
+ {DateTime.Now}
+ Passed: {_results!.Values.Select(r => r.Passed.Count).Sum()}
+ Failed: {_results.Values.Select(r => r.Failed.Count).Sum()}
+ Inconclusive: {_results.Values.Select(r => r.Inconclusive.Count).Sum()}
+ """);
+
+ foreach (KeyValuePair result in _results)
+ {
+ resultBuilder.AppendLine($"""
+
+ ## {result.Key}
+ Passed: {result.Value.Passed.Count}
+ Failed: {result.Value.Failed.Count}
+ Inconclusive: {result.Value.Inconclusive.Count}
+ """);
+
+ foreach (KeyValuePair methodResult in result.Value.Passed)
+ {
+ resultBuilder.AppendLine($"""
+ ### {methodResult.Key} - Passed
+ {methodResult.Value}
+ """);
+ }
+
+ foreach (KeyValuePair methodResult in result.Value.Failed)
+ {
+ resultBuilder.AppendLine($"""
+ ### {methodResult.Key} - Failed
+ {methodResult.Value}
+ """);
+ }
+
+ foreach (KeyValuePair methodResult in result.Value.Inconclusive)
+ {
+ resultBuilder.AppendLine($"""
+ ### {methodResult.Key} - Inconclusive
+ {methodResult.Value}
+ """);
+ }
+ }
+
+ await TestLogger.Log(resultBuilder.ToString());
+
+ await InvokeAsync(async () =>
+ {
+ StateHasChanged();
+ await Task.Delay(1000, token);
+ _running = false;
+ });
+
+ return _results.Values.All(r => r.Failed.Count == 0);
+ }
+
+ private async Task OnTestResults(TestResult result)
+ {
+ _results![result.ClassName] = result;
+ await SaveResults();
+ await InvokeAsync(StateHasChanged);
+
+ if (_settings.StopOnFail && result.Failed.Count > 0)
+ {
+ await CancelRun();
+ await ScrollAndOpenClass(result.ClassName);
+ }
+ }
+
+ private void ToggleAll()
+ {
+ _showAll = !_showAll;
+
+ foreach (TestWrapper? component in _testComponents.Values)
+ {
+ component?.Toggle(_showAll);
+ }
+ }
+
+ private async Task ScrollAndOpenClass(string className)
+ {
+ await _jsTestRunner!.InvokeVoidAsync("scrollToTestClass", className);
+ TestWrapper? testClass = _testComponents[className];
+ testClass?.Toggle(true);
+ }
+
+ private async Task CancelRun()
+ {
+ await _jsTestRunner!.InvokeVoidAsync("setWaitCursor", false);
+ await Task.Yield();
+
+ await InvokeAsync(async () =>
+ {
+ await _cts.CancelAsync();
+ _cts = new CancellationTokenSource();
+ _running = false;
+ });
+ }
+
+ private async ValueTask OnLocationChanging(LocationChangingContext context)
+ {
+ await SaveResults();
+ }
+
+ private async Task SaveResults()
+ {
+ await _jsTestRunner!.InvokeVoidAsync("saveTestResults", _results);
+ }
+
+ private async Task SaveSettings()
+ {
+ await _jsTestRunner!.InvokeVoidAsync("saveSettings", _settings);
+ }
+
+ private async Task LoadSettings()
+ {
+ TestSettings? settings = await _jsTestRunner!.InvokeAsync("loadSettings");
+
+ if (settings is not null)
+ {
+ _settings = settings;
+ }
+ }
+
+ private MarkupString BuildResultSummaryLine(string testName, TestResult result)
+ {
+ StringBuilder builder = new(testName);
+ builder.Append(" - ");
+
+ if (result.Pending > 0)
+ {
+ builder.Append($"Pending: {result.Pending} ");
+ }
+
+ if (result.Passed.Count > 0 || result.Failed.Count > 0 || result.Inconclusive.Count > 0)
+ {
+ if (result.Pending > 0)
+ {
+ builder.Append(" | ");
+ }
+ builder.Append($"Passed: {result.Passed.Count} ");
+ builder.Append(" | ");
+ builder.Append($"Failed: {result.Failed.Count} ");
+ if (result.Inconclusive.Count > 0)
+ {
+ builder.Append(" | ");
+ builder.Append($"Failed: {result.Inconclusive.Count} ");
+ }
+ }
+
+ return new MarkupString(builder.ToString());
+ }
+
+ private int Remaining => _results?.Sum(r =>
+ r.Value.TestCount - (r.Value.Passed.Count + r.Value.Failed.Count + r.Value.Inconclusive.Count)) ?? 0;
+ private int Passed => _results?.Sum(r => r.Value.Passed.Count) ?? 0;
+ private int Failed => _results?.Sum(r => r.Value.Failed.Count) ?? 0;
+ private int Inconclusive => _results?.Sum(r => r.Value.Inconclusive.Count) ?? 0;
+ private IJSObjectReference? _jsTestRunner;
+ private Dictionary? _results;
+ private bool _running;
+ private readonly List _testClassTypes = [];
+ private List _testClassNames = [];
+ private readonly Dictionary _testComponents = new();
+ private bool _showAll;
+ private CancellationTokenSource _cts = new();
+ private TestSettings _settings = new(false, true);
+ private bool _allPassed;
+
+ public record TestSettings(bool StopOnFail, bool RetainResultsOnReload)
+ {
+ public bool StopOnFail { get; set; } = StopOnFail;
+ public bool RetainResultsOnReload { get; set; } = RetainResultsOnReload;
+ }
+
+ private record WFSServer(string Url, string OutputFormat);
+}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/TestFrame.razor b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/TestFrame.razor
index 9d24aa4e7..1db753396 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/TestFrame.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Pages/TestFrame.razor
@@ -124,7 +124,10 @@
private Dictionary Parameters => new()
{
- { nameof(OnTestResults), EventCallback.Factory.Create(this, OnTestResults) },
+ {
+ nameof(OnTestResults),
+ EventCallback.Factory.Create(this, OnTestResults)
+ },
{ nameof(Results), _results }
};
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/TestResult.cs b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/TestResult.cs
index 5a3c4f570..9e2656ff5 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/TestResult.cs
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/TestResult.cs
@@ -2,9 +2,16 @@
namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared;
-public record TestResult(string ClassName, int TestCount,
- Dictionary Passed, Dictionary Failed,
- bool InProgress);
+public record TestResult(
+ string ClassName,
+ int TestCount,
+ Dictionary Passed,
+ Dictionary Failed,
+ Dictionary Inconclusive,
+ bool InProgress)
+{
+ public int Pending => TestCount - (Passed.Count + Failed.Count);
+}
public record ErrorEventArgs(Exception Exception, string MethodName);
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/dymaptic.GeoBlazor.Core.Test.Blazor.Shared.csproj b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/dymaptic.GeoBlazor.Core.Test.Blazor.Shared.csproj
index 11d4efe98..895f3ae72 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/dymaptic.GeoBlazor.Core.Test.Blazor.Shared.csproj
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/dymaptic.GeoBlazor.Core.Test.Blazor.Shared.csproj
@@ -18,16 +18,17 @@
-
+
-
+
-
+
+
-
+
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/css/site.css b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/css/site.css
index 80985b99d..67872984c 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/css/site.css
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/css/site.css
@@ -91,3 +91,31 @@ button {
.blazor-error-boundary::after {
content: "An error has occurred."
}
+
+.passed {
+ color: green;
+}
+
+.failed {
+ color: red;
+}
+
+.pending {
+ color: orange;
+}
+
+.completed {
+ color: blue;
+}
+
+.inconclusive {
+ color: gray;
+}
+
+.bold {
+ font-weight: bold;
+}
+
+.stop-btn {
+ background-color: hotpink;
+}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/testRunner.js b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/testRunner.js
index 4599fbc0b..a3240d3cd 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/testRunner.js
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/wwwroot/testRunner.js
@@ -6,7 +6,7 @@ export let SimpleRenderer;
let esriConfig;
-export function initialize(core) {
+export function initialize(core, wfsServers) {
Core = core;
arcGisObjectRefs = Core.arcGisObjectRefs;
Color = Core.Color;
@@ -14,6 +14,31 @@ export function initialize(core) {
SimpleRenderer = Core.SimpleRenderer;
esriConfig = Core.esriConfig;
setWaitCursor()
+
+ if (!wfsServers) {
+ return;
+ }
+
+ core.esriConfig.request.interceptors.push({
+ before: (params) => {
+ if (wfsServers) {
+ for (let server of wfsServers) {
+ let serverUrl = server.url;
+ if (params.url.includes(serverUrl)) {
+ let serverOutputFormat = server.outputFormat;
+ let requestType = getCaseInsensitive(params.requestOptions.query, 'request');
+ let outputFormat = getCaseInsensitive(params.requestOptions.query, 'outputFormat');
+
+ if (requestType.toLowerCase() === 'getfeature' && !outputFormat) {
+ params.requestOptions.query.outputFormat = serverOutputFormat;
+ }
+ let path = params.url.replace('https://', '');
+ params.url = params.url.replace(serverUrl, `https://${location.host}/sample/wfs/url?url=${path}`);
+ }
+ }
+ }
+ }
+ })
}
export function setWaitCursor(wait) {
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Unit/Usings.cs b/test/dymaptic.GeoBlazor.Core.Test.Unit/Usings.cs
index ab67c7ea9..eefb0a27d 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Unit/Usings.cs
+++ b/test/dymaptic.GeoBlazor.Core.Test.Unit/Usings.cs
@@ -1 +1,4 @@
-global using Microsoft.VisualStudio.TestTools.UnitTesting;
\ No newline at end of file
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+
+[assembly: Parallelize(Scope = ExecutionScope.ClassLevel)]
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Program.cs b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Program.cs
index 586720efc..0ba9aff60 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Program.cs
+++ b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Program.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using dymaptic.GeoBlazor.Core;
+using dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Logging;
using dymaptic.GeoBlazor.Core.Test.WebApp.Client;
using Microsoft.Extensions.Hosting;
@@ -9,5 +10,10 @@
builder.Configuration.AddInMemoryCollection();
builder.Services.AddGeoBlazor(builder.Configuration);
builder.Services.AddScoped();
+builder.Services.AddHttpClient(client =>
+ client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
+builder.Services.AddScoped();
+builder.Services.AddHttpClient(client =>
+ client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
await builder.Build().RunAsync();
diff --git a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Routes.razor b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Routes.razor
index 5c98e3946..a762a4da4 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Routes.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/Routes.razor
@@ -3,8 +3,21 @@
NotFoundPage="@typeof(NotFound)">
-
-
+
+
+
+
+
+
+
+@code {
+ [Parameter]
+ [EditorRequired]
+ public required bool RunOnStart { get; set; }
+
+ [Parameter]
+ public string? TestFilter { get; set; }
+}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/WasmApplicationLifetime.cs b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/WasmApplicationLifetime.cs
index 8879ed19e..0ff0ca0d1 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/WasmApplicationLifetime.cs
+++ b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp.Client/WasmApplicationLifetime.cs
@@ -3,16 +3,18 @@
namespace dymaptic.GeoBlazor.Core.Test.WebApp.Client;
-public class WasmApplicationLifetime: IHostApplicationLifetime
+public class WasmApplicationLifetime(IHttpClientFactory httpClientFactory) : IHostApplicationLifetime
{
- public CancellationToken ApplicationStarted => CancellationToken.None;
+ private readonly CancellationTokenSource _stoppingCts = new();
+ private readonly CancellationTokenSource _stoppedCts = new();
- public CancellationToken ApplicationStopping => CancellationToken.None;
-
- public CancellationToken ApplicationStopped => CancellationToken.None;
+ public CancellationToken ApplicationStarted => CancellationToken.None; // Already started in WASM
+ public CancellationToken ApplicationStopping => _stoppingCts.Token;
+ public CancellationToken ApplicationStopped => _stoppedCts.Token;
public void StopApplication()
{
- throw new NotImplementedException();
+ using HttpClient httpClient = httpClientFactory.CreateClient(nameof(WasmApplicationLifetime));
+ _ = httpClient.PostAsync($"exit?exitCode={Environment.ExitCode}", null);
}
}
\ No newline at end of file
diff --git a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp/Components/App.razor b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp/Components/App.razor
index 8bed667a1..6c0f0fe51 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp/Components/App.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.WebApp/dymaptic.GeoBlazor.Core.Test.WebApp/Components/App.razor
@@ -6,11 +6,12 @@
-
+
+
@@ -24,7 +25,9 @@
@{
#endif
}
-
+