summaryrefslogtreecommitdiff
path: root/src/output_pane_integration_test.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/output_pane_integration_test.zig')
-rw-r--r--src/output_pane_integration_test.zig336
1 files changed, 336 insertions, 0 deletions
diff --git a/src/output_pane_integration_test.zig b/src/output_pane_integration_test.zig
new file mode 100644
index 00000000..25acbbbd
--- /dev/null
+++ b/src/output_pane_integration_test.zig
@@ -0,0 +1,336 @@
+//! End-to-end output-pane tests. Production output identity, rendering, and
+//! builders stay in output_pane.zig; this module exercises their direct seam
+//! with Pardes builtins and the generic n/N input walk.
+const std = @import("std");
+const pardes = @import("pardes.zig");
+const output_pane = @import("output_pane.zig");
+const runtime_config = @import("runtime_config.zig");
+const builtins = @import("builtins.zig");
+const config = @import("config.zig");
+const modal = @import("modal.zig");
+
+const Pardes = pardes.Pardes;
+const Key = pardes.Key;
+const platform = pardes.platform;
+const font_picker = pardes.font_picker;
+const fonts = if (font_picker) @import("fonts.zig") else struct {};
+
+test "Config prints the startup path and refreshes its one output" {
+ const path = "/home/pardes-test/.config/pardes";
+ const p = try Pardes.init(std.testing.allocator, .{ .startup_config_path = path });
+ defer p.deinit();
+
+ try std.testing.expect(p.executeBuiltinLine(0, "Config"));
+ const opened = p.active;
+ const out = p.panes[opened].?.file.?;
+ for ([_][]const u8{
+ "Startup config: " ++ path ++ "\n",
+ "Theme: helix\n",
+ "Shell requested (new panes): " ++ config.default_shell ++ " (default)\n",
+ "Shell effective (last spawn): (none)\n",
+ "Shell pending: on\n",
+ }) |line| try std.testing.expect(std.mem.indexOf(u8, out.content, line) != null);
+ try std.testing.expectEqualStrings(config.config_buffer, std.fs.path.basename(out.path));
+ try std.testing.expectEqual(output_pane.Origin{ .cmd = .Config }, out.output.?.from);
+
+ try std.testing.expect(p.executeBuiltinLine(0, "Config"));
+ try std.testing.expectEqual(opened, p.active);
+ var buffers: usize = 0;
+ for (p.panes) |slot| {
+ const f = (slot orelse continue).file orelse continue;
+ const origin = (f.output orelse continue).from;
+ buffers += @intFromBool(std.meta.eql(origin, output_pane.Origin{ .cmd = .Config }));
+ }
+ try std.testing.expectEqual(@as(usize, 1), buffers);
+}
+
+test "Config reports the absence of a per-user config path" {
+ const p = try Pardes.init(std.testing.allocator, .{});
+ defer p.deinit();
+ try std.testing.expect(p.executeBuiltinLine(0, "Config"));
+ const report = p.panes[p.active].?.file.?.content;
+ try std.testing.expect(std.mem.indexOf(u8, report, "no per-user config path") != null);
+}
+
+test "EffectCode opens the embedded implementation used by this backend" {
+ if (comptime platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+
+ try std.testing.expect(p.executeBuiltinLine(0, "EffectCode PanelSlide"));
+ const out = p.panes[p.active].?.file.?;
+ try std.testing.expectEqualStrings(config.effect_code_buffer, std.fs.path.basename(out.path));
+ try std.testing.expectEqual(output_pane.Origin{ .cmd = .EffectCode }, out.output.?.from);
+ try std.testing.expectEqualStrings("PanelSlide", out.output.?.arg());
+ try std.testing.expect(std.mem.indexOf(u8, out.content, "pub const Transition = enum") != null);
+ const backend_source = switch (platform) {
+ .tty => "src/tty/panel_compositor.zig",
+ .gui => if (@import("pardes_config").gui_shader_sources_prebuilt)
+ "shaders/prebuilt/ui.vert.glsl"
+ else
+ "shaders/ui.vert.glsl",
+ .macos => "src/macos/Sources/ScenePostprocessor.swift",
+ .web => unreachable,
+ };
+ try std.testing.expect(std.mem.indexOf(u8, out.content, backend_source) != null);
+}
+
+test "every enabled setting builtin mutates the State Config reports" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ try std.testing.expectEqual(config.gui_tagline_font_percent, p.settings.font.tagline_percent);
+
+ var font_arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer font_arena.deinit();
+ var chosen_font: ?[]const u8 = null;
+ var buf: [512]u8 = undefined;
+ for (runtime_config.settings) |setting| {
+ if (!setting.enabled(builtins.capabilities)) continue;
+ const command: []const u8 = switch (setting.action) {
+ .theme => try std.fmt.bufPrint(&buf, "{s} acme", .{setting.word}),
+ .shell => try std.fmt.bufPrint(&buf, "{s} fish", .{setting.word}),
+ .tagline_size => try std.fmt.bufPrint(&buf, "{s} 67", .{setting.word}),
+ .font => continue,
+ else => setting.word,
+ };
+ try std.testing.expect(p.executeBuiltinLine(p.active, command));
+ }
+ if (comptime font_picker) {
+ const before_invalid = p.settings.font.tagline_percent;
+ try std.testing.expect(p.executeBuiltinLine(p.active, "TaglineSize 0"));
+ try std.testing.expectEqual(before_invalid, p.settings.font.tagline_percent);
+ const installed = fonts.list(font_arena.allocator(), null);
+ if (installed.len > 0) {
+ chosen_font = installed[0].name;
+ const command = try std.fmt.bufPrint(&buf, "Font {s}", .{installed[0].name});
+ try std.testing.expect(p.executeBuiltinLine(p.active, command));
+ }
+ }
+
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Config"));
+ const report = p.panes[p.active].?.file.?.content;
+ for ([_][]const u8{
+ "Colors: off\n",
+ "Wrap: off\n",
+ "Tagbottom: on\n",
+ "Debug: on\n",
+ "Theme: acme\n",
+ "Shell requested (new panes): fish\n",
+ }) |line| try std.testing.expect(std.mem.indexOf(u8, report, line) != null);
+ const transition = if (builtins.capabilities.panel_transitions)
+ try std.fmt.bufPrint(&buf, "Panel transition: {s}\n", .{
+ runtime_config.findAction(.{ .transition = p.settings.panel_transition }).?.word,
+ })
+ else
+ "Panel transition: unsupported\n";
+ try std.testing.expect(std.mem.indexOf(u8, report, transition) != null);
+ const scene_status = if (builtins.capabilities.scene_shaders) "on" else "unsupported";
+ for ([_][]const u8{ "Crt", "Ripple", "Glitch" }) |name| {
+ const line = try std.fmt.bufPrint(&buf, "{s}: {s}\n", .{ name, scene_status });
+ try std.testing.expect(std.mem.indexOf(u8, report, line) != null);
+ }
+ const tagline = if (!builtins.capabilities.tagline_font_size)
+ "TaglineSize: unsupported\n"
+ else
+ try std.fmt.bufPrint(&buf, "TaglineSize: {d}%{s}\n", .{
+ p.settings.font.tagline_percent,
+ if (builtins.capabilities.font_picker) "" else " (build-time only)",
+ });
+ try std.testing.expect(std.mem.indexOf(u8, report, tagline) != null);
+ if (chosen_font) |name| {
+ try std.testing.expect(std.mem.indexOf(u8, report, name) != null);
+ try std.testing.expect(std.mem.indexOf(u8, report, "Font pending: on\n") != null);
+ }
+}
+
+fn walkFixture(p: *Pardes, id: usize, cwd: []const u8, pattern: []const u8) !usize {
+ const rows = try p.gpa.dupe(u8,
+ \\build.zig:1:1 first
+ \\(mise.toml) and build.zig.zon:3:2-9 two on one row
+ \\nothing look-able on this row at all
+ \\uucode_config.zig:7:1 last
+ \\
+ );
+ try output_pane.fillResults(p, id, cwd, .search, pattern, rows, null);
+ return p.panes[id].?.search_pane orelse error.MissingResults;
+}
+
+const ProjectPaths = struct { cwd: []const u8, boot: []const u8 };
+
+fn projectPaths(cwd_buf: *[4096]u8, path_buf: *[4096]u8) !ProjectPaths {
+ const raw = std.c.getcwd(cwd_buf, cwd_buf.len) orelse return error.GetCwdFailed;
+ const cwd = std.mem.span(@as([*:0]u8, @ptrCast(raw)));
+ return .{ .cwd = cwd, .boot = try std.fmt.bufPrint(path_buf, "{s}/mise.toml", .{cwd}) };
+}
+
+fn selectedOutputText(pane: *const pardes.Pane) ?[]const u8 {
+ const file = pane.file orelse return null;
+ if (!pane.vsel.active or pane.vsel.row != pane.cur_row or pane.cur_row < 0) return null;
+ const line = modal.lineSlice(file.content, @intCast(pane.cur_row));
+ const lo: usize = @intCast(@max(0, @min(pane.vsel.col, pane.cur_col)));
+ const hi: usize = @intCast(@max(0, @max(pane.vsel.col, pane.cur_col)));
+ if (lo >= line.len) return "";
+ return line[lo..@min(line.len, hi + 1)];
+}
+
+test "n/N selects one output location per row and opens nothing" {
+ if (platform == .web) return;
+ var cwd_buf: [4096]u8 = undefined;
+ var path_buf: [4096]u8 = undefined;
+ const paths = try projectPaths(&cwd_buf, &path_buf);
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot });
+ defer p.deinit();
+ p.update(.{ .resize = .{ .cols = 100, .rows = 40 } });
+ const rid = try walkFixture(p, p.active, paths.cwd, "one");
+ const results = p.panes[rid].?;
+ const panes_before = p.freeSlot();
+
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(rid, p.active);
+ try std.testing.expectEqual(panes_before, p.freeSlot());
+ try std.testing.expect(results.vsel.active and results.vsel.explicit);
+ try std.testing.expectEqualStrings("build.zig:1:1", selectedOutputText(results) orelse "");
+ try std.testing.expectEqual(output_pane.Grain.line, output_pane.grain(results.file.?.output));
+
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(@as(i32, 1), results.cur_row);
+ try std.testing.expectEqualStrings("mise.toml", selectedOutputText(results) orelse "");
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(@as(i32, 3), results.cur_row); // row 2 is not look-able
+ try std.testing.expectEqualStrings("uucode_config.zig:7:1", selectedOutputText(results) orelse "");
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(@as(i32, 0), results.cur_row); // ring seam
+
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(p.freeSlot() != panes_before);
+ try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/build.zig"));
+ try std.testing.expectEqual(output_pane.Grain.word, output_pane.grain(p.panes[p.active].?.file.?.output));
+}
+
+test "n/N resumes the result output whose Look moved focus away" {
+ if (platform == .web) return;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(std.testing.io, .{
+ .sub_path = "look-owner.txt",
+ .data = "alpha target\nbeta target\ngamma target\n",
+ });
+ var path_buf: [256]u8 = undefined;
+ const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/look-owner.txt", .{tmp.sub_path});
+ const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 });
+ defer p.deinit();
+
+ try std.testing.expect(p.executeBuiltinLine(0, "Look target"));
+ const rid = p.panes[0].?.search_pane orelse return error.MissingResults;
+ const results = p.panes[rid].?;
+ const first = results.cur_row;
+ try std.testing.expectEqual(@as(usize, 0), p.active);
+ try std.testing.expect(results.look_at != null);
+ try std.testing.expectEqual(results.serial, p.look_walk_owner orelse return error.MissingLookOwner);
+
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(rid, p.active);
+ try std.testing.expectEqual(first + 1, results.cur_row);
+ p.update(.{ .key = .{ .cp = 'N' } });
+ try std.testing.expectEqual(first, results.cur_row);
+
+ // A later no-match answer has no position to resume and therefore cannot
+ // steal the provenance established by the Look above.
+ const owner = results.serial;
+ const dir = std.fs.path.dirname(path) orelse ".";
+ try output_pane.fillResults(
+ p,
+ 0,
+ dir,
+ .search,
+ "no-such-result",
+ try p.gpa.dupe(u8, ""),
+ null,
+ );
+ try std.testing.expectEqual(owner, p.look_walk_owner.?);
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(rid, p.active);
+ try std.testing.expectEqual(first + 1, results.cur_row);
+
+ // Emptying the owner itself keeps its identity as the ring's starting
+ // point. Deleting it then leaves a stale serial, never an id that can bind
+ // to the unrelated pane subsequently allocated in the same slot.
+ try output_pane.fillResults(p, 0, dir, .search, "target", try p.gpa.dupe(u8, ""), null);
+ try std.testing.expectEqual(owner, p.look_walk_owner.?);
+ try std.testing.expect(p.executeBuiltinLine(rid, "Del"));
+ try std.testing.expect(p.paneBySerial(owner) == null);
+ const replacement = try p.newShell(rid, "");
+ try std.testing.expect(replacement.serial != owner);
+ try std.testing.expect(p.paneBySerial(owner) == null);
+}
+
+test "N exactly reverses n across output panes and the ring seam" {
+ if (platform == .web) return;
+ var cwd_buf: [4096]u8 = undefined;
+ var path_buf: [4096]u8 = undefined;
+ const paths = try projectPaths(&cwd_buf, &path_buf);
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot });
+ defer p.deinit();
+ p.update(.{ .resize = .{ .cols = 100, .rows = 40 } });
+ const first = try walkFixture(p, p.active, paths.cwd, "one");
+ p.update(.{ .key = .{ .cp = 'n', .alt = true } });
+ const second = try walkFixture(p, p.active, paths.cwd, "two");
+ try std.testing.expect(first != second);
+
+ const Mark = struct { pane: usize, row: i32, col: i32 };
+ const here = struct {
+ fn at(pp: *Pardes) Mark {
+ const pane = pp.panes[pp.active].?;
+ return .{ .pane = pp.active, .row = pane.cur_row, .col = pane.cur_col };
+ }
+ }.at;
+ p.update(.{ .key = .{ .cp = 'n' } });
+ const base = here(p);
+ var trail: [12]Mark = undefined;
+ for (&trail) |*mark| {
+ p.update(.{ .key = .{ .cp = 'n' } });
+ mark.* = here(p);
+ }
+ var i = trail.len;
+ while (i > 0) {
+ i -= 1;
+ p.update(.{ .key = .{ .cp = 'N' } });
+ const want = if (i == 0) base else trail[i - 1];
+ try std.testing.expectEqual(want, here(p));
+ }
+ var saw_first = false;
+ var saw_second = false;
+ for (trail) |mark| {
+ saw_first = saw_first or mark.pane == first;
+ saw_second = saw_second or mark.pane == second;
+ }
+ try std.testing.expect(saw_first and saw_second);
+}
+
+test "n/N selects command outputs by whole row" {
+ if (platform == .web) return;
+ var cwd_buf: [4096]u8 = undefined;
+ var path_buf: [4096]u8 = undefined;
+ const paths = try projectPaths(&cwd_buf, &path_buf);
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot });
+ defer p.deinit();
+ p.update(.{ .resize = .{ .cols = 100, .rows = 40 } });
+
+ try std.testing.expect(p.executeBuiltinLine(p.active, "ThemeSel"));
+ const tid = p.panes[p.active].?.search_pane orelse return error.MissingThemeList;
+ const themes = p.panes[tid].?;
+ try std.testing.expect(output_pane.fileTraits(themes.file.?.output).commands);
+ p.update(.{ .key = .{ .cp = 'n' } });
+ const row0 = std.mem.trimEnd(u8, modal.lineSlice(themes.file.?.content, 0), " \t\r");
+ try std.testing.expectEqualStrings(row0, selectedOutputText(themes) orelse "");
+ const theme_before = p.settings.theme;
+ p.update(.{ .key = .{ .cp = 'n' } });
+ p.update(.{ .key = .{ .cp = Key.tab } });
+ try std.testing.expect(p.settings.theme != theme_before);
+
+ try std.testing.expect(p.executeBuiltinLine(0, "Look build.zig"));
+ p.update(.tick);
+ try std.testing.expect(p.active != 0);
+ p.update(.{ .key = .{ .cp = 'n' } });
+ try std.testing.expectEqual(@as(usize, 0), p.active);
+}