diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /src/output_pane_integration_test.zig | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz pardes-1551e409c31992437cb2fa864f576d45c8433801.zip | |
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/output_pane_integration_test.zig')
| -rw-r--r-- | src/output_pane_integration_test.zig | 336 |
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); +} |
