using Bunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared; /// /// Regression tests for MonacoEditor, which wrapped every JS /// interop call in a bare try { ... } catch { } with no logging — a /// genuine Monaco init failure became invisible. The fix narrows the catch to /// the expected prerender / disconnect cases and logs any real /// via ILogger. /// public class MonacoEditorLoggingTests : BunitContext { /// Captures log entries so the test can assert on them. private sealed class CapturingLoggerProvider : ILoggerProvider { /// Gets the log entries captured so far. public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new(); /// Creates a logger that appends to the shared entry list. /// The logger category name (ignored). /// A capturing logger. public ILogger CreateLogger(string categoryName) => new CapturingLogger(Entries); /// Does nothing; the provider holds no resources. public void Dispose() { } private sealed class CapturingLogger : ILogger { private readonly List<(LogLevel, string, Exception?)> _entries; /// Initializes the logger over the shared entry list. /// The list that receives each captured log entry. public CapturingLogger(List<(LogLevel, string, Exception?)> entries) => _entries = entries; /// Returns no scope; scopes are not captured. /// The scope state type. /// The scope state (ignored). /// Always null. public IDisposable? BeginScope(TState state) where TState : notnull => null; /// Reports every log level as enabled. /// The log level being queried. /// Always true. public bool IsEnabled(LogLevel logLevel) => true; /// Records the formatted log entry for later assertions. /// The log state type. /// The entry's log level. /// The entry's event id. /// The entry's state. /// The exception attached to the entry, if any. /// Formats the state and exception into the message. public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => _entries.Add((logLevel, formatter(state, exception), exception)); } } /// Verifies that a genuine JavaScript exception during editor creation is logged rather than swallowed. [Fact] public void CreateEditor_GenuineJsException_IsLogged_NotSwallowed() { var provider = new CapturingLoggerProvider(); Services.AddLogging(b => b.AddProvider(provider)); // createEditor is an InvokeVoidAsync call — configure it to throw a // genuine JSException so we exercise the real-failure path. JSInterop.Mode = JSRuntimeMode.Strict; JSInterop.SetupVoid("MonacoBlazor.createEditor", _ => true) .SetException(new JSException("Monaco failed to load")); // Pre-fix: the bare catch {} swallowed this with no trace. Post-fix: // the component renders fine but the failure is logged. var cut = Render(p => p.Add(c => c.ShowToolbar, false)); var errors = provider.Entries.Where(e => e.Level == LogLevel.Error).ToList(); Assert.NotEmpty(errors); Assert.Contains(errors, e => e.Exception is JSException); } /// Verifies that editor creation during prerender does not log anything. [Fact] public void CreateEditor_Prerender_DoesNotLog() { // When JS interop is unavailable (prerender), createEditor throws // InvalidOperationException — that is expected and must NOT be logged. var provider = new CapturingLoggerProvider(); Services.AddLogging(b => b.AddProvider(provider)); JSInterop.Mode = JSRuntimeMode.Strict; JSInterop.SetupVoid("MonacoBlazor.createEditor", _ => true) .SetException(new InvalidOperationException("JS interop not available during prerender")); var cut = Render(p => p.Add(c => c.ShowToolbar, false)); Assert.DoesNotContain(provider.Entries, e => e.Level >= LogLevel.Warning); } }