Files
att_wm/src/test.zig
T

425 lines
18 KiB
Zig

const std = @import("std");
const testing = std.testing;
const act = @import("action");
const input = @import("input");
const layout = @import("layout.zig");
const Box = layout.Box;
comptime {
_ = act;
_ = input;
_ = @import("color.zig");
}
const area: Box = .{ .x = 0, .y = 0, .width = 1000, .height = 600 };
fn params(nmaster: u32, mfact: f32) layout.Params {
return .{ .area = area, .nmaster = nmaster, .mfact = mfact };
}
// ─── master/stack tiling ─────────────────────────────────────────────────────
test "single window fills the whole area" {
var cells: [1]Box = undefined;
_ = layout.arrange(.master, params(1, 0.55), &cells);
try testing.expectEqual(area, cells[0]);
}
test "two windows split at mfact" {
var cells: [2]Box = undefined;
_ = layout.arrange(.master, params(1, 0.55), &cells);
try testing.expectEqual(@as(i32, 550), cells[0].width);
try testing.expectEqual(@as(i32, 600), cells[0].height);
try testing.expectEqual(@as(i32, 550), cells[1].x);
try testing.expectEqual(@as(i32, 450), cells[1].width);
try testing.expectEqual(@as(i32, 600), cells[1].height);
}
test "stack column divides height with no gap or overlap" {
// Three windows, one master: the stack column holds two.
var cells: [3]Box = undefined;
_ = layout.arrange(.master, params(1, 0.5), &cells);
try testing.expectEqual(@as(i32, 600), cells[0].height);
try testing.expectEqual(cells[1].y + cells[1].height, cells[2].y);
try testing.expectEqual(area.height, cells[1].height + cells[2].height);
}
test "odd heights are absorbed rather than leaving a gap at the bottom" {
// 600 / 7 does not divide evenly; the last window must still end exactly
// at the bottom edge.
var cells: [7]Box = undefined;
_ = layout.arrange(.master, params(0, 0.55), &cells);
var y: i32 = area.y;
for (cells) |cell| {
try testing.expectEqual(y, cell.y);
y += cell.height;
}
try testing.expectEqual(area.y + area.height, y);
}
test "nmaster zero puts every window in the stack column" {
var cells: [3]Box = undefined;
_ = layout.arrange(.master, params(0, 0.55), &cells);
for (cells) |cell| {
try testing.expectEqual(@as(i32, 0), cell.x);
try testing.expectEqual(area.width, cell.width);
}
}
test "windows all fit in master when count does not exceed nmaster" {
var cells: [2]Box = undefined;
_ = layout.arrange(.master, params(3, 0.55), &cells);
// No stack column, so master spans the full width.
for (cells) |cell| {
try testing.expectEqual(area.width, cell.width);
}
try testing.expectEqual(area.height, cells[0].height + cells[1].height);
}
test "master column also divides height when nmaster exceeds one" {
var cells: [4]Box = undefined;
_ = layout.arrange(.master, params(2, 0.5), &cells);
try testing.expectEqual(@as(i32, 500), cells[0].width);
try testing.expectEqual(@as(i32, 500), cells[1].width);
try testing.expectEqual(area.height, cells[0].height + cells[1].height);
try testing.expectEqual(area.height, cells[2].height + cells[3].height);
}
// ─── stacking layouts ────────────────────────────────────────────────────────
test "monocle gives every window the full area and reports stacked" {
var cells: [3]Box = undefined;
const result = layout.arrange(.monocle, params(1, 0.55), &cells);
try testing.expect(result.stacked);
try testing.expect(result.tabbar == null);
for (cells) |cell| try testing.expectEqual(area, cell);
}
test "master does not report stacked" {
var cells: [2]Box = undefined;
var p = params(1, 0.55);
p.tabbar_height = 22;
try testing.expect(!layout.arrange(.master, p, &cells).stacked);
}
test "tabbed reserves the bar strip above the windows" {
var cells: [3]Box = undefined;
var p = params(1, 0.55);
p.tabbar_height = 22;
const result = layout.arrange(.tabbed, p, &cells);
try testing.expect(result.stacked);
const bar = result.tabbar.?;
try testing.expectEqual(@as(i32, 22), bar.height);
try testing.expectEqual(area.y, bar.y);
// Windows start below the bar and the two together cover the area exactly.
for (cells) |cell| {
try testing.expectEqual(area.y + 22, cell.y);
try testing.expectEqual(area.height - 22, cell.height);
}
}
test "tabbed drops the bar rather than crushing the windows" {
var cells: [2]Box = undefined;
var p: layout.Params = .{
.area = .{ .x = 0, .y = 0, .width = 400, .height = 30 },
.nmaster = 1,
.mfact = 0.55,
.tabbar_height = 22,
};
const result = layout.arrange(.tabbed, p, &cells);
try testing.expect(result.tabbar == null);
try testing.expectEqual(@as(i32, 30), cells[0].height);
p.tabbar_height = 0;
}
test "no windows is not a crash" {
var cells: [0]Box = undefined;
const result = layout.arrange(.master, params(1, 0.55), &cells);
try testing.expect(result.tabbar == null);
}
// ─── gaps ────────────────────────────────────────────────────────────────────
test "outer gap insets the whole area" {
var cells: [1]Box = undefined;
var p = params(1, 0.55);
p.outer_gap = 10;
_ = layout.arrange(.master, p, &cells);
try testing.expectEqual(@as(i32, 10), cells[0].x);
try testing.expectEqual(@as(i32, 10), cells[0].y);
try testing.expectEqual(@as(i32, 980), cells[0].width);
try testing.expectEqual(@as(i32, 580), cells[0].height);
}
test "inner gap separates adjacent windows" {
var cells: [2]Box = undefined;
var p = params(1, 0.5);
p.gap = 10;
_ = layout.arrange(.master, p, &cells);
// Each cell shrinks by half the gap per side, leaving a full gap between.
const right_of_master = cells[0].x + cells[0].width;
try testing.expect(cells[1].x - right_of_master == 10);
}
// ─── tab rectangles ──────────────────────────────────────────────────────────
test "tab rects tile the bar exactly with no rounding gap" {
const bar: Box = .{ .x = 5, .y = 0, .width = 101, .height = 22 };
var rects: [4]Box = undefined;
layout.tabRects(bar, 4, &rects);
try testing.expectEqual(bar.x, rects[0].x);
var total: i32 = 0;
for (rects, 0..) |rect, i| {
total += rect.width;
if (i > 0) {
try testing.expectEqual(rects[i - 1].x + rects[i - 1].width, rect.x);
}
}
try testing.expectEqual(bar.width, total);
try testing.expectEqual(bar.x + bar.width, rects[3].x + rects[3].width);
}
test "single tab spans the bar" {
const bar: Box = .{ .x = 0, .y = 0, .width = 300, .height = 22 };
var rects: [1]Box = undefined;
layout.tabRects(bar, 1, &rects);
try testing.expectEqual(@as(i32, 300), rects[0].width);
}
// ─── Box helpers ─────────────────────────────────────────────────────────────
test "inset never produces negative dimensions" {
const tiny: Box = .{ .x = 0, .y = 0, .width = 4, .height = 4 };
const r = tiny.inset(10);
try testing.expectEqual(@as(i32, 0), r.width);
try testing.expectEqual(@as(i32, 0), r.height);
}
test "contains is half open on the far edges" {
const b: Box = .{ .x = 10, .y = 10, .width = 100, .height = 50 };
try testing.expect(b.contains(10, 10));
try testing.expect(b.contains(109, 59));
try testing.expect(!b.contains(110, 30));
try testing.expect(!b.contains(9, 30));
}
// ─── per-tag settings slots ──────────────────────────────────────────────────
test "each single tag gets its own settings slot" {
for (0..act.tag_count) |i| {
const mask = @as(u32, 1) << @intCast(i);
try testing.expectEqual(i + 1, act.tagSlot(mask));
}
}
test "views of more than one tag share the shared slot" {
try testing.expectEqual(@as(usize, 0), act.tagSlot(0b11));
try testing.expectEqual(@as(usize, 0), act.tagSlot(0b101));
try testing.expectEqual(@as(usize, 0), act.tagSlot(act.all_tags));
}
test "an empty view falls back to the shared slot" {
try testing.expectEqual(@as(usize, 0), act.tagSlot(0));
}
test "bits above the tag range do not affect the slot" {
// A single valid tag stays on its own slot even with junk in the high bits,
// so a mask that survived a sloppy IPC caller cannot index past the array.
const junk: u32 = ~act.all_tags;
try testing.expectEqual(@as(usize, 1), act.tagSlot(0b1 | junk));
try testing.expectEqual(act.tag_count, act.tagSlot(@as(u32, 1) << (act.tag_count - 1)));
}
// ─── command parsing ─────────────────────────────────────────────────────────
fn parseOk(argv: []const []const u8) act.Action {
return act.parse(argv) catch unreachable;
}
test "tag arguments accept indices, masks and all" {
try testing.expectEqual(@as(u32, 1), parseOk(&.{ "view", "1" }).view);
try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "3" }).view);
try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "0x4" }).view);
try testing.expectEqual(@as(u32, 4), parseOk(&.{ "view", "mask:4" }).view);
try testing.expectEqual(act.all_tags, parseOk(&.{ "view", "all" }).view);
}
test "tag indices outside the range are rejected" {
try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "0" }));
try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "10" }));
try testing.expectError(error.InvalidArgument, act.parse(&.{ "view", "nope" }));
try testing.expectError(error.MissingArgument, act.parse(&.{"view"}));
}
test "masks are clamped to the valid tag range" {
try testing.expectEqual(act.all_tags, parseOk(&.{ "view", "0xffffffff" }).view);
}
test "deltas distinguish relative from absolute" {
switch (parseOk(&.{ "mfact", "+0.05" }).mfact) {
.relative => |v| try testing.expectApproxEqAbs(@as(f32, 0.05), v, 1e-6),
.absolute => return error.TestUnexpectedResult,
}
switch (parseOk(&.{ "mfact", "0.5" }).mfact) {
.absolute => |v| try testing.expectApproxEqAbs(@as(f32, 0.5), v, 1e-6),
.relative => return error.TestUnexpectedResult,
}
switch (parseOk(&.{ "nmaster", "-1" }).nmaster) {
.relative => |v| try testing.expectEqual(@as(i32, -1), v),
.absolute => return error.TestUnexpectedResult,
}
}
test "delta application respects relative and absolute" {
const rel = act.Delta(i32){ .relative = -1 };
const abs = act.Delta(i32){ .absolute = 3 };
try testing.expectEqual(@as(i32, 4), rel.apply(5));
try testing.expectEqual(@as(i32, 3), abs.apply(5));
}
test "unknown commands are rejected rather than guessed at" {
try testing.expectError(error.UnknownCommand, act.parse(&.{"nonsense"}));
try testing.expectError(error.UnknownCommand, act.parse(&.{}));
}
test "spawn keeps its whole argv" {
const a = parseOk(&.{ "spawn", "foot", "-e", "htop" });
try testing.expectEqual(@as(usize, 3), a.spawn.len);
try testing.expectEqualStrings("htop", a.spawn[2]);
try testing.expectError(error.MissingArgument, act.parse(&.{"spawn"}));
}
test "directions parse both spellings" {
try testing.expectEqual(act.Direction.next, parseOk(&.{ "focus", "next" }).focus);
try testing.expectEqual(act.Direction.prev, parseOk(&.{ "focus", "prev" }).focus);
try testing.expectEqual(act.Direction.prev, parseOk(&.{ "focus", "previous" }).focus);
try testing.expectError(error.InvalidArgument, act.parse(&.{ "focus", "sideways" }));
}
test "window commands keep the identifier verbatim" {
try testing.expectEqualStrings("w-17", parseOk(&.{ "focus-window", "w-17" }).focus_window);
try testing.expectEqualStrings("w-17", parseOk(&.{ "close-window", "w-17" }).close_window);
// An identifier is opaque, so a direction-looking one is still an id.
try testing.expectEqualStrings("next", parseOk(&.{ "focus-window", "next" }).focus_window);
try testing.expectError(error.MissingArgument, act.parse(&.{"focus-window"}));
try testing.expectError(error.InvalidArgument, act.parse(&.{ "focus-window", "" }));
}
test "layouts parse by name" {
try testing.expectEqual(act.Layout.monocle, parseOk(&.{ "layout", "monocle" }).set_layout);
try testing.expectEqual(act.Layout.tabbed, parseOk(&.{ "layout", "tabbed" }).set_layout);
try testing.expectError(error.InvalidArgument, act.parse(&.{ "layout", "spiral" }));
}
test "only navigation-style actions repeat on key hold" {
try testing.expect(parseOk(&.{ "focus", "next" }).repeats());
try testing.expect(parseOk(&.{ "mfact", "+0.05" }).repeats());
try testing.expect(!parseOk(&.{"zoom"}).repeats());
try testing.expect(!parseOk(&.{"close"}).repeats());
try testing.expect(!parseOk(&.{ "view", "1" }).repeats());
}
// ─── input device rules ──────────────────────────────────────────────────────
test "device name globs match the way a config author expects" {
const touchpad = "ELAN0501:00 04F3:3060 Touchpad";
try testing.expect(input.matches("*Touchpad*", touchpad));
try testing.expect(input.matches("*Touchpad", touchpad));
try testing.expect(input.matches("ELAN*", touchpad));
try testing.expect(input.matches("*", touchpad));
try testing.expect(input.matches(touchpad, touchpad));
try testing.expect(!input.matches("*Trackpoint*", touchpad));
try testing.expect(!input.matches("Touchpad", touchpad));
// A literal pattern must match the whole name, not merely a prefix.
try testing.expect(!input.matches("ELAN0501", touchpad));
}
test "globs handle empty runs and repeated stars" {
try testing.expect(input.matches("", ""));
try testing.expect(input.matches("*", ""));
try testing.expect(input.matches("***", ""));
try testing.expect(!input.matches("a", ""));
// The backtracking case: each star has to be willing to give ground.
try testing.expect(input.matches("*a*b*c*", "xxaxxbxxcxx"));
try testing.expect(!input.matches("*a*b*c*", "xxaxxcxxbxx"));
// Only the last 'a' lets the rest of the pattern through.
try testing.expect(input.matches("*aab", "aaab"));
}
test "rules match on name and type independently" {
const rule: input.Rule = .{ .name = "*Touchpad*", .type = .pointer, .tap = true };
try testing.expect(rule.matchesDevice("Foo Touchpad", .pointer));
// Right name, wrong kind of device.
try testing.expect(!rule.matchesDevice("Foo Touchpad", .touch));
try testing.expect(!rule.matchesDevice("Foo Keyboard", .pointer));
// A rule with neither selector applies to everything.
const catch_all: input.Rule = .{ .natural_scroll = true };
try testing.expect(catch_all.matchesDevice("anything", .tablet));
try testing.expect(catch_all.matchesDevice("", .keyboard));
}
test "later rules override earlier ones field by field" {
const broad: input.Rule = .{ .name = "*", .tap = true, .natural_scroll = true };
const narrow: input.Rule = .{ .name = "*Touchpad*", .tap = false, .click_method = .clickfinger };
const merged = (input.Rule{}).merge(broad).merge(narrow);
// Stated twice: the later rule wins.
try testing.expectEqual(@as(?bool, false), merged.tap);
// Stated only by the broad rule: survives.
try testing.expectEqual(@as(?bool, true), merged.natural_scroll);
// Stated only by the narrow rule: applied.
try testing.expectEqual(@as(?input.ClickMethod, .clickfinger), merged.click_method);
// Stated by neither: still null, so the device keeps libinput's default.
try testing.expectEqual(@as(?bool, null), merged.middle_emulation);
}
test "merging leaves the selectors alone" {
// Otherwise a merged rule would claim to be about whichever device matched
// last, which is not a thing anything should be able to read back out.
const merged = (input.Rule{ .name = "a", .type = .pointer })
.merge(.{ .name = "b", .type = .keyboard, .tap = true });
try testing.expectEqualStrings("a", merged.name.?);
try testing.expectEqual(@as(?input.Type, .pointer), merged.type);
try testing.expectEqual(@as(?bool, true), merged.tap);
}
test "an unset keymap is recognised as the default" {
try testing.expect((input.Keymap{}).isDefault());
try testing.expect(!(input.Keymap{ .layout = "us" }).isDefault());
try testing.expect(!(input.Keymap{ .options = "caps:escape" }).isDefault());
}
test "map_to_output merges like any other setting" {
// It is a string rather than a scalar, so worth pinning that the generic
// merge handles it and that a rule silent about it does not clear it.
const merged = (input.Rule{})
.merge(.{ .type = .touch, .map_to_output = "eDP-1" })
.merge(.{ .name = "*", .tap = true });
try testing.expectEqualStrings("eDP-1", merged.map_to_output.?);
// And that a later rule naming a different output does win.
const moved = merged.merge(.{ .map_to_output = "DP-2" });
try testing.expectEqualStrings("DP-2", moved.map_to_output.?);
}