Kaynağa Gözat

Merge pull request #316 from VirSanctus/bug/256

Fix notes editor overwriting input while typing (#256)
Tim Jones 1 gün önce
ebeveyn
işleme
875c19351e

+ 12 - 5
Shared.Rcl/Components/MarkdownEditor.razor

@@ -23,8 +23,9 @@
     <textarea
         class="w-full h-64 bg-zinc-950 text-zinc-200 border border-zinc-700 rounded p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500"
         data-testid="@($"{BaseTestId}-textarea")"
-        value="@Value"
-        @oninput="HandleInput">
+        @bind="_text"
+        @bind:event="oninput"
+        @bind:after="NotifyChanged">
     </textarea>
 
 </div>
@@ -45,10 +46,16 @@
             ? "markdown-editor"
             : $"{TestIdPrefix}-markdown-editor";
 
-    async Task HandleInput(ChangeEventArgs e)
+    private string? _text;
+
+    protected override void OnParametersSet()
+    {
+        _text = Value;
+    }
+
+    Task NotifyChanged()
     {
-        Value = e.Value?.ToString();
-        await ValueChanged.InvokeAsync(Value);
+        return ValueChanged.InvokeAsync(_text);
     }
 
     async Task HandleSave()

+ 66 - 0
Tests.E2e/AccessPointCardTests.cs

@@ -457,4 +457,70 @@ public class AccessPointCardTests(
             await context.CloseAsync();
         }
     }
+
+    [Fact]
+    public async Task User_Can_Type_In_The_Middle_Of_Notes_Without_The_Cursor_Jumping() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        await BlazorLatency.AddAsync(page, TimeSpan.FromMilliseconds(50));
+        var name = $"e2e-ap-{Guid.NewGuid():N}"[..16];
+
+        try {
+            AccessPointCardPom card = await CreateAccessPointAsync(page, name);
+
+            await card.BeginEditAsync(name);
+            await card.SetNotesAsync(name, "Line one\nLine three");
+            await card.TypeNotesLineAfterFirstLineAsync(name, "Line two typed in the middle");
+
+            await Assertions.Expect(card.NotesEditorTextarea(name))
+                .ToHaveValueAsync("Line one\nLine two typed in the middle\nLine three");
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+
+    [Fact]
+    public async Task User_Can_Type_Notes_Quickly_Over_A_Slow_Connection_And_Save() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+        await BlazorLatency.AddAsync(page, TimeSpan.FromMilliseconds(50));
+        var name = $"e2e-ap-{Guid.NewGuid():N}"[..16];
+        var notes = "This is currently in bridge mode and uplinked to the core switch.";
+
+        try {
+            AccessPointCardPom card = await CreateAccessPointAsync(page, name);
+
+            await card.BeginEditAsync(name);
+            await card.TypeNotesAsync(name, notes);
+
+            await Assertions.Expect(card.NotesEditorTextarea(name)).ToHaveValueAsync(notes);
+
+            await card.SaveAsync(name);
+
+            await Assertions.Expect(card.NotesViewerContent(name)).ToHaveTextAsync(notes);
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+
+    private async Task<AccessPointCardPom> CreateAccessPointAsync(IPage page, string name) {
+        await page.GotoAsync(_fixture.BaseUrl);
+
+        var layout = new MainLayoutPom(page);
+        await layout.AssertLoadedAsync();
+        await layout.GotoHardwareAsync();
+
+        var hardwareTree = new HardwareTreePom(page);
+        await hardwareTree.AssertLoadedAsync();
+        await hardwareTree.GotoAccessPointsListAsync();
+
+        var list = new AccessPointsListPom(page);
+        await list.AssertLoadedAsync();
+        await list.AddAccessPointAsync(name);
+        await page.WaitForURLAsync($"**/resources/hardware/{name}");
+
+        var card = new AccessPointCardPom(page);
+        await card.AssertCardVisibleAsync(name);
+        return card;
+    }
 }

+ 41 - 0
Tests.E2e/Infra/BlazorLatency.cs

@@ -0,0 +1,41 @@
+using System.Text.RegularExpressions;
+using System.Threading.Channels;
+using Microsoft.Playwright;
+
+namespace Tests.E2e.Infra;
+
+/// <summary>
+///     Simulates a slow network between the browser and the Blazor Server circuit
+///     by delaying every SignalR WebSocket frame in both directions. Frame order is preserved.
+///     Must be called before the page navigates, so the circuit's socket is routed.
+/// </summary>
+public static partial class BlazorLatency {
+    public static Task AddAsync(IPage page, TimeSpan oneWayDelay) =>
+        page.RouteWebSocketAsync(BlazorSocket(), ws => {
+            IWebSocketRoute server = ws.ConnectToServer();
+            ws.OnMessage(DelayedForwarder(oneWayDelay, server));
+            server.OnMessage(DelayedForwarder(oneWayDelay, ws));
+        });
+
+    private static Action<IWebSocketFrame> DelayedForwarder(TimeSpan delay, IWebSocketRoute target) {
+        var queue = Channel.CreateUnbounded<(DateTime Due, string? Text, byte[]? Binary)>();
+
+        _ = Task.Run(async () => {
+            await foreach ((DateTime due, var text, var binary) in queue.Reader.ReadAllAsync()) {
+                TimeSpan wait = due - DateTime.UtcNow;
+                if (wait > TimeSpan.Zero)
+                    await Task.Delay(wait);
+
+                if (binary is not null)
+                    target.Send(binary);
+                else
+                    target.Send(text ?? string.Empty);
+            }
+        });
+
+        return frame => queue.Writer.TryWrite((DateTime.UtcNow + delay, frame.Text, frame.Binary));
+    }
+
+    [GeneratedRegex("/_blazor")]
+    private static partial Regex BlazorSocket();
+}

+ 16 - 0
Tests.E2e/PageObjectModels/AccessPointCardPom.cs

@@ -119,6 +119,22 @@ public class AccessPointCardPom(IPage page) {
     public async Task SetSpeedAsync(string accessPointName, double speed) =>
         await SpeedInput(accessPointName).FillAsync(speed.ToString(CultureInfo.InvariantCulture));
 
+    public async Task SetNotesAsync(string accessPointName, string notes) =>
+        await NotesEditorTextarea(accessPointName).FillAsync(notes);
+
+    public async Task TypeNotesAsync(string accessPointName, string text) =>
+        await NotesEditorTextarea(accessPointName).PressSequentiallyAsync(
+            text,
+            new LocatorPressSequentiallyOptions { Delay = 30 });
+
+    public async Task TypeNotesLineAfterFirstLineAsync(string accessPointName, string line) {
+        await NotesEditorTextarea(accessPointName).FocusAsync();
+        await page.Keyboard.PressAsync("Control+Home");
+        await page.Keyboard.PressAsync("End");
+        await page.Keyboard.PressAsync("Enter");
+        await TypeNotesAsync(accessPointName, line);
+    }
+
     public async Task SaveAsync(string accessPointName) {
         await SaveButton(accessPointName).ClickAsync();
         await Assertions.Expect(ModelSection(accessPointName)).ToBeVisibleAsync();