using System.Text.RegularExpressions;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.ScriptAnalysis;
///
/// Source pins for the Monaco interop (AdminUI has no bUnit and no JS test runner). These do not prove the
/// browser behaviour — the docker-dev live check does — they stop the specific regressions R04:P1/P9/S2/S3 fixed
/// from quietly coming back: per-keystroke text sync, unsequenced diagnostics, the inlay-hints POST, and an
/// unconsumed marker round-trip.
///
[Trait("Category", "Unit")]
public sealed class MonacoInteropSourceTests
{
private static string Read(params string[] relative)
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx")))
dir = dir.Parent;
dir.ShouldNotBeNull("could not locate the repository root");
return File.ReadAllText(Path.Combine(new[] { dir!.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.AdminUI" }.Concat(relative).ToArray()));
}
private static string Js() => Read("wwwroot", "js", "monaco-init.js");
[Fact]
public void The_content_change_handler_does_not_ship_the_text_to_dotnet()
{
var js = Js();
var handler = Regex.Match(js, @"onDidChangeModelContent\(function \(\) \{(?
.*?)\n \}\);", RegexOptions.Singleline);
handler.Success.ShouldBeTrue("the content-change handler moved; update this pin");
handler.Groups["body"].Value.ShouldNotContain("OnValueChanged");
handler.Groups["body"].Value.ShouldNotContain("getValue");
// Positive control: the text IS still reported — at blur.
js.ShouldContain("onDidBlurEditorText");
js.ShouldContain("\"OnValueChanged\"");
}
[Fact]
public void Diagnostics_are_generation_guarded_and_abortable()
{
var js = Js();
js.ShouldContain("diagGeneration");
js.ShouldContain("generation !== e.diagGeneration");
js.ShouldContain("new AbortController()");
js.ShouldContain("signal: signal");
}
[Fact]
public void Providers_honour_the_monaco_cancellation_token()
{
var js = Js();
js.ShouldContain("token.onCancellationRequested");
foreach (var path in new[] { "\"completions\"", "\"hover\"", "\"format\"", "\"signature-help\"" })
js.ShouldContain("postAnalysis(" + path);
}
[Fact]
public void The_inlay_hints_provider_is_not_registered() => Js().ShouldNotContain("registerInlayHintsProvider");
[Fact]
public void Markers_cross_to_dotnet_only_for_a_subscribed_problems_list()
{
Js().ShouldContain("if (e.reportMarkers) {");
Read("Components", "Shared", "MonacoEditor.razor").ShouldContain("reportMarkers = MarkersChanged.HasDelegate");
}
[Fact]
public void The_markers_report_is_capped_before_it_crosses_the_circuit()
{
// Review I-1: the only OnMarkersChanged invoke sends the capped report, never the raw marker array.
var js = Js();
Regex.Matches(js, "\"OnMarkersChanged\"").Count.ShouldBe(1);
js.ShouldContain("dotNetRef.invokeMethodAsync(\"OnMarkersChanged\", report.markers, report.total)");
js.ShouldContain("const report = markerReport(e, markers);");
js.ShouldContain(".slice(0, max)");
js.ShouldContain("m.message.substring(0, maxChars - 1)");
// The caps come from the C# constants, so the JS and ScriptProblemReport.FromEditor agree.
var razor = Read("Components", "Shared", "MonacoEditor.razor");
razor.ShouldContain("maxReportedMarkers = ScriptEditorLimits.MaxReportedMarkers");
razor.ShouldContain("maxReportedMessageChars = ScriptEditorLimits.MaxReportedMessageChars");
razor.ShouldContain("ScriptProblemReport.FromEditor(markers, total)");
}
[Fact]
public void An_oversize_text_is_announced_at_blur_not_only_at_save()
{
Js().ShouldContain("invokeMethodAsync(\"OnSyncRefused\", serializedBytes(v))");
Read("Components", "Shared", "MonacoEditor.razor").ShouldContain("public void OnSyncRefused(int bytes)");
}
[Theory]
[InlineData("Components", "Pages", "ScriptEdit.razor")]
[InlineData("Components", "Shared", "Uns", "VirtualTagModal.razor")]
public void Script_hosts_read_the_editor_on_save_and_render_the_problems_list(params string[] path)
{
var src = Read(path);
src.ShouldContain("GetValueAsync()");
src.ShouldContain("