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("