#74

Testing text widgets.

Opentomislav-ivankovicopened 1y ago

I am struggling to find a way to test whether or not a text widget is rendered on the UI. I have tried ImGuiTestContext::ItemCheck and ImGuiTestContext::ItemExists, but these seem to work only with interactive widgets and they always fail to find text. I am struggling to test this simple logs window UI:

const std = @import("std");
const imgui = @import("imgui");
const log = @import("../log/root.zig");
const ui = @import("../ui/root.zig");

pub fn logsWindow(comptime buffer_logger: type, open: ?*bool) void {
    if (imgui.igBegin("Logs", open, imgui.ImGuiWindowFlags_HorizontalScrollbar)) {
        const storage = imgui.igGetStateStorage();
        const is_scroll_at_bottom_id = imgui.igGetID_Str("is_scroll_at_bottom");
        const is_scroll_at_bottom = imgui.ImGuiStorage_GetBool(storage, is_scroll_at_bottom_id, true);
        {
            const entries = buffer_logger.lockAndGetEntries();
            defer buffer_logger.unlock();
            for (0..entries.len) |index| {
                const entry = entries.get(index) catch continue;
                const color = getLogColor(entry.level);
                imgui.igTextColored(color, "%s", entry.full_message.ptr);
            }
        }
        if (is_scroll_at_bottom) {
            imgui.igSetScrollHereY(1.0);
        }
        imgui.ImGuiStorage_SetBool(
            storage,
            is_scroll_at_bottom_id,
            imgui.igGetScrollY() >= imgui.igGetScrollMaxY() - 1.0,
        );
    }
    imgui.igEnd();
}

fn getLogColor(log_level: std.log.Level) imgui.ImVec4 {
    return switch (log_level) {
        .err => .{ .x = 1, .y = 0.5, .z = 0.5, .w = 1 },
        .warn => .{ .x = 1, .y = 1, .z = 0, .w = 1 },
        .info => .{ .x = 0, .y = 1, .z = 1, .w = 1 },
        .debug => .{ .x = 0.7, .y = 0.7, .z = 0.7, .w = 1 },
    };
}

const testing = std.testing;

test "should render every log message in correct color" {
    const nanoTimestamp = struct {
        fn call() i128 {
            return 1577934245123456789;
        }
    }.call;

    const logger = log.BufferLogger(.{
        .level = .debug,
        .time_zone = .utc,
        .buffer_size = 4096,
        .max_entries = 64,
        .nanoTimestamp = nanoTimestamp,
    });

    logger.logFn(.debug, std.log.default_log_scope, "Message: 1", .{});
    logger.logFn(.info, std.log.default_log_scope, "Message: 2", .{});
    logger.logFn(.warn, std.log.default_log_scope, "Message: 3", .{});
    logger.logFn(.err, std.log.default_log_scope, "Message: 4", .{});

    const context = try ui.getTestingContext();
    try context.runTest(
        .{},
        struct {
            fn call(_: ui.TestContext) !void {
                logsWindow(logger, null);
            }
        }.call,
        struct {
            fn call(ctx: ui.TestContext) !void {
                ctx.setRef("Logs");
                ctx.itemCheck("2020-01-02T03:04:05.123456789 [debug] Message: 1", 0); // Fails.
                // TODO assert that text color is getLogColor(.debug)
                ctx.itemCheck("2020-01-02T03:04:05.123456789 [info] Message: 2", 0); // Fails.
                // TODO assert that text color is getLogColor(.info)
                ctx.itemCheck("2020-01-02T03:04:05.123456789 [warning] Message: 3", 0); // Fails.
                // TODO assert that text color is getLogColor(.warn)
                ctx.itemCheck("2020-01-02T03:04:05.123456789 [error] Message: 4", 0); // Fails.
                // TODO assert that text color is getLogColor(.err)
            }
        }.call,
    );
}

I hope you don't mind that this example code is in ZIG. Asserting that some text exists seems like a pretty fundamental thing for testing UI-s. Is it even possible to do this with this test engine? Am I missing something.

Activity

1
tomislav-ivankovic1y ago

Ok. I figured out how to do it:

const std = @import("std");
const buildin = @import("builtin");
const imgui = @import("imgui");
const log = @import("../log/root.zig");
const ui = @import("../ui/root.zig");

pub fn logsWindow(comptime buffer_logger: type, open: ?*bool) void {
    if (imgui.igBegin("Logs", open, imgui.ImGuiWindowFlags_HorizontalScrollbar)) {
        const storage = imgui.igGetStateStorage();
        const is_scroll_at_bottom_id = imgui.igGetID_Str("is_scroll_at_bottom");
        const is_scroll_at_bottom = imgui.ImGuiStorage_GetBool(storage, is_scroll_at_bottom_id, true);
        {
            const entries = buffer_logger.lockAndGetEntries();
            defer buffer_logger.unlock();
            for (0..entries.len) |index| {
                const entry = entries.get(index) catch continue;
                textColored(getLogColor(entry.level), entry.full_message);
            }
        }
        if (is_scroll_at_bottom) {
            imgui.igSetScrollHereY(1.0);
        }
        imgui.ImGuiStorage_SetBool(
            storage,
            is_scroll_at_bottom_id,
            imgui.igGetScrollY() >= imgui.igGetScrollMaxY() - 1.0,
        );
    }
    imgui.igEnd();
}

fn getLogColor(log_level: std.log.Level) imgui.ImVec4 {
    return switch (log_level) {
        .err => .{ .x = 1, .y = 0.5, .z = 0.5, .w = 1 },
        .warn => .{ .x = 1, .y = 1, .z = 0, .w = 1 },
        .info => .{ .x = 0, .y = 1, .z = 1, .w = 1 },
        .debug => .{ .x = 0.7, .y = 0.7, .z = 0.7, .w = 1 },
    };
}

fn textColored(color: imgui.ImVec4, text: [:0]const u8) void {
    if (buildin.is_test) {
        var pos: imgui.ImVec2 = undefined;
        imgui.igGetCursorScreenPos(&pos);
        var size: imgui.ImVec2 = undefined;
        imgui.igCalcTextSize(&size, text, null, false, -1.0);
        const rect = imgui.ImRect{ .Min = pos, .Max = .{ .x = pos.x + size.x, .y = pos.y + size.y } };
        imgui.teItemAdd(imgui.igGetCurrentContext(), imgui.igGetID_Str(text), &rect, null);
    }
    imgui.igTextColored(color, "%s", text.ptr);
}

const testing = std.testing;

test "should render every log message" {
    const nanoTimestamp = struct {
        fn call() i128 {
            return 1577934245123456789;
        }
    }.call;

    const logger = log.BufferLogger(.{
        .level = .debug,
        .time_zone = .utc,
        .buffer_size = 4096,
        .max_entries = 64,
        .nanoTimestamp = nanoTimestamp,
    });

    logger.logFn(.debug, std.log.default_log_scope, "Message: 1", .{});
    logger.logFn(.info, std.log.default_log_scope, "Message: 2", .{});
    logger.logFn(.warn, std.log.default_log_scope, "Message: 3", .{});
    logger.logFn(.err, std.log.default_log_scope, "Message: 4", .{});

    const context = try ui.getTestingContext();
    try context.runTest(
        .{},
        struct {
            fn call(_: ui.TestContext) !void {
                logsWindow(logger, null);
            }
        }.call,
        struct {
            fn call(ctx: ui.TestContext) !void {
                ctx.setRef("Logs");
                try testing.expect(ctx.itemExists("2020-01-02T03:04:05.123456789 [debug] Message: 1"));
                try testing.expect(ctx.itemExists("2020-01-02T03:04:05.123456789 [info] Message: 2"));
                try testing.expect(ctx.itemExists("2020-01-02T03:04:05.123456789 [warning] Message: 3"));
                try testing.expect(ctx.itemExists("2020-01-02T03:04:05.123456789 [error] Message: 4"));
            }
        }.call,
    );
}

Trick was to call ImGuiTestEngineHook_ItemAdd for every instance of text widget that I want to be accessible to the testing engine.