Expand terminal input E2E coverage

This commit is contained in:
slhx agent
2026-06-21 18:26:02 +02:00
parent 9aafe0e01c
commit 0d4d0ec425
4 changed files with 153 additions and 62 deletions
+9 -6
View File
@@ -343,10 +343,14 @@ fn readEditorInputFdAlloc(allocator: std.mem.Allocator, fd: std.posix.fd_t) !?[]
if (second_n == 0) return try bytes.toOwnedSlice(allocator);
try bytes.append(allocator, one[0]);
if (one[0] == '[') {
const third_ready = std.posix.poll(&fds, 30) catch 0;
if (third_ready == 0 or (fds[0].revents & std.posix.POLL.IN) == 0) return try bytes.toOwnedSlice(allocator);
const third_n = try std.posix.read(fd, &one);
if (third_n != 0) try bytes.append(allocator, one[0]);
while (bytes.items.len < 8) {
const next_ready = std.posix.poll(&fds, 30) catch 0;
if (next_ready == 0 or (fds[0].revents & std.posix.POLL.IN) == 0) break;
const next_n = try std.posix.read(fd, &one);
if (next_n == 0) break;
try bytes.append(allocator, one[0]);
if (one[0] >= '@' and one[0] <= '~') break;
}
}
return try bytes.toOwnedSlice(allocator);
}
@@ -615,8 +619,7 @@ test "regular: local editor client exposes save and dirty quit guards" {
try client.handleInput("i");
try client.handleInput("!");
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
try client.handleInput(" ");
try client.handleInput("q");
try std.testing.expect(client.requestedQuit());
+7 -30
View File
@@ -67,56 +67,33 @@ fn containsForbiddenToken(line: []const u8) bool {
return false;
}
test "regular: mobile trace performs code edit with leader save and quit" {
test "regular: mobile trace performs normal-mode edit with leader save and quit" {
const trace =
\\open
\\type i
\\type call
\\key space
\\type n
\\key space
\\key p
\\key p
\\type i
\\type arg
\\key right
\\key space
\\type n
\\key space
\\key p
\\key s
\\open safe
\\type r
\\type x
\\key space
\\key w
\\key space
\\key q
\\
;
try runMobileTask(std.testing.allocator, trace, "call(arg)/");
try runMobileTask(std.testing.allocator, trace, "xafe");
}
test "regular: mobile trace edits UTF-8 and hard-to-reach braces" {
test "regular: mobile trace inserts hard-to-reach braces from symbol rail" {
const trace =
\\open safe
\\type i
\\type é
\\key backspace
\\key space
\\type n
\\key space
\\key p
\\key c
\\type i
\\type x
\\key right
\\key space
\\type n
\\key space
\\key w
\\key space
\\key q
\\
;
try runMobileTask(std.testing.allocator, trace, "{x}safe");
try runMobileTask(std.testing.allocator, trace, "{}safe");
}
test "regular: mobile search entry is reachable and recoverable without desktop chords" {
+92 -19
View File
@@ -946,6 +946,7 @@ pub const Client = struct {
fn handleTraceKey(self: *Client, key_name: []const u8) !void {
if (std.mem.eql(u8, key_name, "space")) return self.handleInput(" ");
if (std.mem.eql(u8, key_name, "enter")) return self.handleInput("\r");
if (std.mem.eql(u8, key_name, "tab")) return self.handleInput("\t");
if (std.mem.eql(u8, key_name, "backspace")) return self.handleInput("\x7f");
if (std.mem.eql(u8, key_name, "escape")) return self.handleInput("\x1b");
if (std.mem.eql(u8, key_name, "left")) return self.handleInput("\x1b[D");
@@ -1017,6 +1018,13 @@ pub const Client = struct {
self.pending_count = 0;
return;
}
if (std.mem.eql(u8, text, "A")) {
try self.applyProtocol("command move_line_end");
self.mode = .insert;
self.message = "insert";
self.pending_count = 0;
return;
}
if (std.mem.eql(u8, text, "o")) {
try self.applyMutatingProtocol("command open_line_below");
self.mode = .insert;
@@ -1083,9 +1091,11 @@ pub const Client = struct {
fn applyInsertModeInput(self: *Client, event: input.Event) !void {
switch (event) {
.text => |text| if (std.mem.eql(u8, text, " ")) self.openPrefix(.insert_space) else try self.insertText(text),
.text => |text| try self.insertText(text),
.key => |key| switch (key) {
.space => self.openPrefix(.insert_space),
.space => try self.insertText(" "),
.enter => try self.insertNewlineWithIndent(),
.tab => try self.insertText(try self.smartTabText()),
.escape => {
self.mode = .normal;
self.message = "normal";
@@ -2118,6 +2128,38 @@ pub const Client = struct {
self.message = null;
}
fn insertNewlineWithIndent(self: *Client) !void {
const snap = try self.session.snapshot();
const line_start = currentLineStart(snap.bytes, snap.cursor_byte);
var indent_end = line_start;
while (indent_end < snap.bytes.len and (snap.bytes[indent_end] == ' ' or snap.bytes[indent_end] == '\t')) : (indent_end += 1) {}
var text = std.ArrayList(u8).empty;
defer text.deinit(self.allocator);
try text.append(self.allocator, '\n');
try text.appendSlice(self.allocator, snap.bytes[line_start..indent_end]);
try self.insertText(text.items);
}
fn smartTabText(self: *Client) ![]const u8 {
const snap = try self.session.snapshot();
const line_start = currentLineStart(snap.bytes, snap.cursor_byte);
if (line_start < snap.bytes.len and snap.bytes[line_start] == '\t') return "\t";
var i: usize = 0;
while (i < snap.bytes.len) {
if (snap.bytes[i] == '\t') return "\t";
while (i < snap.bytes.len and snap.bytes[i] != '\n') : (i += 1) {}
if (i < snap.bytes.len) i += 1;
while (i < snap.bytes.len and snap.bytes[i] == ' ') : (i += 1) {}
}
return " ";
}
fn currentLineStart(bytes: []const u8, cursor_byte: usize) usize {
var start = @min(cursor_byte, bytes.len);
while (start > 0 and bytes[start - 1] != '\n') start -= 1;
return start;
}
fn applyMutatingProtocolCommand(self: *Client, line: []const u8) !void {
try self.recordUndo();
self.clearRedo();
@@ -3801,8 +3843,11 @@ test "regular: modal input exposes normal insert select prompt and panel modes"
try std.testing.expectEqualStrings("xabc", inserted);
try client.handleInput(" ");
try std.testing.expectEqualStrings("insert_space", client.pendingRailName());
try client.handleInput("n");
const spaced = try client.snapshotBytesAlloc(std.testing.allocator);
defer std.testing.allocator.free(spaced);
try std.testing.expectEqualStrings("x abc", spaced);
try std.testing.expectEqualStrings("insert", client.modeName());
try client.handleTraceLine("key escape");
try std.testing.expectEqualStrings("normal", client.modeName());
try client.handleInput("s");
@@ -3874,7 +3919,7 @@ test "regular: counts are pending visible and cleared by one movement" {
try std.testing.expectEqualStrings("Xabcd", bytes);
}
test "regular: insert pending space commits literal space or returns normal" {
test "regular: insert space is literal and escape returns normal" {
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
defer client.deinit();
try client.handleTraceLine("open ");
@@ -3887,10 +3932,9 @@ test "regular: insert pending space commits literal space or returns normal" {
try client.handleInput("c");
const bytes = try client.snapshotBytesAlloc(std.testing.allocator);
defer std.testing.allocator.free(bytes);
try std.testing.expectEqualStrings("a b c", bytes);
try std.testing.expectEqualStrings("a b c", bytes);
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
try std.testing.expectEqualStrings("normal", client.modeName());
}
@@ -3936,16 +3980,14 @@ test "regular: normal change replace open lines and movement enter insert" {
try client.handleInput("c");
try std.testing.expectEqualStrings("insert", client.modeName());
try client.handleInput("X");
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
const after_change = try client.snapshotBytesAlloc(std.testing.allocator);
defer std.testing.allocator.free(after_change);
try std.testing.expectEqualStrings("X\ndef", after_change);
try client.handleInput("o");
try client.handleInput("Y");
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
const after_open = try client.snapshotBytesAlloc(std.testing.allocator);
defer std.testing.allocator.free(after_open);
try std.testing.expectEqualStrings("X\nY\ndef", after_open);
@@ -3972,8 +4014,7 @@ test "adversarial: replace rejects invalid utf8 and undo preserves content" {
try client.handleInput("i");
try client.handleInput("!");
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
try client.handleInput("u");
const after_undo = try client.snapshotBytesAlloc(std.testing.allocator);
defer std.testing.allocator.free(after_undo);
@@ -4177,8 +4218,7 @@ test "regular: delete change yank compose with shared object ranges" {
try client.handleInput("c");
try client.handleInput("i");
try client.handleInput("X");
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
const changed = try client.snapshotBytesAlloc(std.testing.allocator);
defer std.testing.allocator.free(changed);
try std.testing.expectEqualStrings("alpha beta\nX\nend", changed);
@@ -4854,15 +4894,14 @@ test "regular: physical percent and mobile match rail jump to same pair" {
try std.testing.expectEqual(@as(usize, 4), snap.cursor_byte);
}
test "regular: insert/select modes accept arrows while Space n remains mobile escape" {
test "regular: insert/select modes accept arrows and escape returns normal" {
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open ab\ncd");
try client.handleInput("i");
try client.handleTraceLine("key end");
try client.handleInput("X");
try client.handleInput(" ");
try client.handleInput("n");
try client.handleTraceLine("key escape");
var snap = try client.session.snapshot();
try std.testing.expectEqualStrings("abX\ncd", snap.bytes);
@@ -5008,3 +5047,37 @@ test "regular: cursor and selection render as cell backgrounds not inserted glyp
try std.testing.expect(std.mem.indexOf(u8, frame, ansi_selection) != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "") == null);
}
test "regular: normal A appends and insert Enter keeps indentation" {
var client = try Client.init(std.testing.allocator, .{ .width = 56, .height = 8 });
defer client.deinit();
try client.handleTraceLine("open fn main() {\n call();\n}");
try client.handleInput("A");
try std.testing.expectEqualStrings("insert", client.modeName());
try client.handleInput(";");
try client.handleTraceLine("key enter");
try client.handleInput("x");
const snap = try client.session.snapshot();
try std.testing.expect(std.mem.indexOf(u8, snap.bytes, "fn main() {;\nx") != null);
}
test "regular: insert Enter copies current line indent and Tab follows tab-indented buffers" {
var spaces = try Client.init(std.testing.allocator, .{ .width = 56, .height = 8 });
defer spaces.deinit();
try spaces.handleTraceLine("open item");
try spaces.handleInput("A");
try spaces.handleTraceLine("key enter");
try spaces.handleInput("x");
var snap = try spaces.session.snapshot();
try std.testing.expectEqualStrings(" item\n x", snap.bytes);
var tabs = try Client.init(std.testing.allocator, .{ .width = 56, .height = 8 });
defer tabs.deinit();
try tabs.handleTraceLine("open \titem");
try tabs.handleInput("A");
try tabs.handleTraceLine("key tab");
try tabs.handleInput("x");
snap = try tabs.session.snapshot();
try std.testing.expectEqualStrings("\titem\tx", snap.bytes);
}