513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
101 lines
5.1 KiB
C#
101 lines
5.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Regression tests for <c>MonacoEditor</c>, which wrapped every JS
|
|
/// interop call in a bare <c>try { ... } catch { }</c> 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
|
|
/// <see cref="JSException"/> via <c>ILogger</c>.
|
|
/// </summary>
|
|
public class MonacoEditorLoggingTests : BunitContext
|
|
{
|
|
/// <summary>Captures log entries so the test can assert on them.</summary>
|
|
private sealed class CapturingLoggerProvider : ILoggerProvider
|
|
{
|
|
/// <summary>Gets the log entries captured so far.</summary>
|
|
public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();
|
|
|
|
/// <summary>Creates a logger that appends to the shared entry list.</summary>
|
|
/// <param name="categoryName">The logger category name (ignored).</param>
|
|
/// <returns>A capturing logger.</returns>
|
|
public ILogger CreateLogger(string categoryName) => new CapturingLogger(Entries);
|
|
/// <summary>Does nothing; the provider holds no resources.</summary>
|
|
public void Dispose() { }
|
|
|
|
private sealed class CapturingLogger : ILogger
|
|
{
|
|
private readonly List<(LogLevel, string, Exception?)> _entries;
|
|
/// <summary>Initializes the logger over the shared entry list.</summary>
|
|
/// <param name="entries">The list that receives each captured log entry.</param>
|
|
public CapturingLogger(List<(LogLevel, string, Exception?)> entries) => _entries = entries;
|
|
|
|
/// <summary>Returns no scope; scopes are not captured.</summary>
|
|
/// <typeparam name="TState">The scope state type.</typeparam>
|
|
/// <param name="state">The scope state (ignored).</param>
|
|
/// <returns>Always null.</returns>
|
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
|
/// <summary>Reports every log level as enabled.</summary>
|
|
/// <param name="logLevel">The log level being queried.</param>
|
|
/// <returns>Always true.</returns>
|
|
public bool IsEnabled(LogLevel logLevel) => true;
|
|
|
|
/// <summary>Records the formatted log entry for later assertions.</summary>
|
|
/// <typeparam name="TState">The log state type.</typeparam>
|
|
/// <param name="logLevel">The entry's log level.</param>
|
|
/// <param name="eventId">The entry's event id.</param>
|
|
/// <param name="state">The entry's state.</param>
|
|
/// <param name="exception">The exception attached to the entry, if any.</param>
|
|
/// <param name="formatter">Formats the state and exception into the message.</param>
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
|
|
Exception? exception, Func<TState, Exception?, string> formatter)
|
|
=> _entries.Add((logLevel, formatter(state, exception), exception));
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that a genuine JavaScript exception during editor creation is logged rather than swallowed.</summary>
|
|
[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<MonacoEditor>(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);
|
|
}
|
|
|
|
/// <summary>Verifies that editor creation during prerender does not log anything.</summary>
|
|
[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<MonacoEditor>(p => p.Add(c => c.ShowToolbar, false));
|
|
|
|
Assert.DoesNotContain(provider.Entries, e => e.Level >= LogLevel.Warning);
|
|
}
|
|
}
|