|
| 1 | +//! Unit tests for the zig-webui bindings. |
| 2 | +//! |
| 3 | +//! The tests are split into two groups: |
| 4 | +//! |
| 5 | +//! 1. Pure-Zig checks that don't touch the C library at all (enum integer |
| 6 | +//! values, error sets, Event layout, comptime helpers, version constants). |
| 7 | +//! 2. Smoke tests that exercise C entry points which do *not* spin up a |
| 8 | +//! browser or block on a server (window create/destroy, free-port, |
| 9 | +//! mime-type lookup, base64 encode/decode, malloc/free, config setters). |
| 10 | +//! |
| 11 | +//! Run with `zig build test`. |
| 12 | + |
| 13 | +const std = @import("std"); |
| 14 | +const builtin = @import("builtin"); |
| 15 | +const webui = @import("webui"); |
| 16 | +const compat_tuple = @import("compat_tuple"); |
| 17 | + |
| 18 | +// ============================================================================= |
| 19 | +// Pure-Zig tests (no C calls) |
| 20 | +// ============================================================================= |
| 21 | + |
| 22 | +test "WEBUI_VERSION matches build.zig.zon" { |
| 23 | + try std.testing.expectEqual(@as(u64, 2), webui.WEBUI_VERSION.major); |
| 24 | + try std.testing.expectEqual(@as(u64, 5), webui.WEBUI_VERSION.minor); |
| 25 | + try std.testing.expectEqual(@as(u64, 0), webui.WEBUI_VERSION.patch); |
| 26 | + try std.testing.expectEqualStrings("beta.4", webui.WEBUI_VERSION.pre.?); |
| 27 | +} |
| 28 | + |
| 29 | +test "constants" { |
| 30 | + try std.testing.expectEqual(@as(usize, 65535), webui.WEBUI_MAX_IDS); |
| 31 | + try std.testing.expectEqual(@as(usize, 16), webui.WEBUI_MAX_ARG); |
| 32 | +} |
| 33 | + |
| 34 | +test "Browser enum integer values match C ABI" { |
| 35 | + try std.testing.expectEqual(@as(usize, 0), @intFromEnum(webui.Browser.NoBrowser)); |
| 36 | + try std.testing.expectEqual(@as(usize, 1), @intFromEnum(webui.Browser.AnyBrowser)); |
| 37 | + try std.testing.expectEqual(@as(usize, 2), @intFromEnum(webui.Browser.Chrome)); |
| 38 | + try std.testing.expectEqual(@as(usize, 13), @intFromEnum(webui.Browser.Webview)); |
| 39 | +} |
| 40 | + |
| 41 | +test "Runtime enum integer values match C ABI" { |
| 42 | + try std.testing.expectEqual(@as(usize, 0), @intFromEnum(webui.Runtime.None)); |
| 43 | + try std.testing.expectEqual(@as(usize, 1), @intFromEnum(webui.Runtime.Deno)); |
| 44 | + try std.testing.expectEqual(@as(usize, 2), @intFromEnum(webui.Runtime.NodeJS)); |
| 45 | + try std.testing.expectEqual(@as(usize, 3), @intFromEnum(webui.Runtime.Bun)); |
| 46 | +} |
| 47 | + |
| 48 | +test "LoggerLevel enum integer values match C ABI" { |
| 49 | + try std.testing.expectEqual(@as(usize, 0), @intFromEnum(webui.LoggerLevel.Debug)); |
| 50 | + try std.testing.expectEqual(@as(usize, 1), @intFromEnum(webui.LoggerLevel.Info)); |
| 51 | + try std.testing.expectEqual(@as(usize, 2), @intFromEnum(webui.LoggerLevel.Error)); |
| 52 | +} |
| 53 | + |
| 54 | +test "EventKind enum integer values match C ABI" { |
| 55 | + try std.testing.expectEqual(@as(usize, 0), @intFromEnum(webui.EventKind.EVENT_DISCONNECTED)); |
| 56 | + try std.testing.expectEqual(@as(usize, 1), @intFromEnum(webui.EventKind.EVENT_CONNECTED)); |
| 57 | + try std.testing.expectEqual(@as(usize, 2), @intFromEnum(webui.EventKind.EVENT_MOUSE_CLICK)); |
| 58 | + try std.testing.expectEqual(@as(usize, 3), @intFromEnum(webui.EventKind.EVENT_NAVIGATION)); |
| 59 | + try std.testing.expectEqual(@as(usize, 4), @intFromEnum(webui.EventKind.EVENT_CALLBACK)); |
| 60 | +} |
| 61 | + |
| 62 | +test "Config enum starts at 0" { |
| 63 | + try std.testing.expectEqual(@as(c_int, 0), @intFromEnum(webui.Config.show_wait_connection)); |
| 64 | + try std.testing.expectEqual(@as(c_int, 1), @intFromEnum(webui.Config.ui_event_blocking)); |
| 65 | +} |
| 66 | + |
| 67 | +test "Event is extern struct with stable layout" { |
| 68 | + try std.testing.expect(@typeInfo(webui.Event) == .@"struct"); |
| 69 | + try std.testing.expectEqual(.@"extern", @typeInfo(webui.Event).@"struct".layout); |
| 70 | + |
| 71 | + // The C side relies on this exact field ordering. |
| 72 | + try std.testing.expectEqual(@as(usize, 0), @offsetOf(webui.Event, "window")); |
| 73 | + const event_type_offset = @offsetOf(webui.Event, "event_type"); |
| 74 | + try std.testing.expect(event_type_offset > 0); |
| 75 | + try std.testing.expect(@offsetOf(webui.Event, "element") > event_type_offset); |
| 76 | + try std.testing.expect(@offsetOf(webui.Event, "cookies") > @offsetOf(webui.Event, "element")); |
| 77 | +} |
| 78 | + |
| 79 | +test "WebUIError contains expected variants" { |
| 80 | + // Compile-time check: every variant we ship is reachable. |
| 81 | + const want: []const webui.WebUIError = &.{ |
| 82 | + webui.WebUIError.GenericError, |
| 83 | + webui.WebUIError.CreateWindowError, |
| 84 | + webui.WebUIError.BindError, |
| 85 | + webui.WebUIError.ShowError, |
| 86 | + webui.WebUIError.ServerError, |
| 87 | + webui.WebUIError.EncodeError, |
| 88 | + webui.WebUIError.DecodeError, |
| 89 | + webui.WebUIError.UrlError, |
| 90 | + webui.WebUIError.ProcessError, |
| 91 | + webui.WebUIError.HWNDError, |
| 92 | + webui.WebUIError.PortError, |
| 93 | + webui.WebUIError.ScriptError, |
| 94 | + webui.WebUIError.AllocateFailed, |
| 95 | + }; |
| 96 | + try std.testing.expectEqual(@as(usize, 13), want.len); |
| 97 | +} |
| 98 | + |
| 99 | +test "compat_tuple.fnParamsToTuple synthesizes correct tuple" { |
| 100 | + const Type = std.builtin.Type; |
| 101 | + const params = [_]Type.Fn.Param{ |
| 102 | + .{ .is_generic = false, .is_noalias = false, .type = i32 }, |
| 103 | + .{ .is_generic = false, .is_noalias = false, .type = bool }, |
| 104 | + .{ .is_generic = false, .is_noalias = false, .type = f64 }, |
| 105 | + }; |
| 106 | + const Tup = compat_tuple.fnParamsToTuple(¶ms); |
| 107 | + |
| 108 | + const info = @typeInfo(Tup).@"struct"; |
| 109 | + try std.testing.expect(info.is_tuple); |
| 110 | + try std.testing.expectEqual(@as(usize, 3), info.fields.len); |
| 111 | + try std.testing.expectEqual(i32, info.fields[0].type); |
| 112 | + try std.testing.expectEqual(bool, info.fields[1].type); |
| 113 | + try std.testing.expectEqual(f64, info.fields[2].type); |
| 114 | + |
| 115 | + // We can build an instance and read it back. |
| 116 | + var t: Tup = undefined; |
| 117 | + t[0] = -7; |
| 118 | + t[1] = true; |
| 119 | + t[2] = 3.5; |
| 120 | + try std.testing.expectEqual(@as(i32, -7), t[0]); |
| 121 | + try std.testing.expect(t[1]); |
| 122 | + try std.testing.expectEqual(@as(f64, 3.5), t[2]); |
| 123 | +} |
| 124 | + |
| 125 | +// ============================================================================= |
| 126 | +// C-backed smoke tests (no browser, no server) |
| 127 | +// ============================================================================= |
| 128 | + |
| 129 | +test "newWindow then destroy roundtrip" { |
| 130 | + const win = webui.newWindow(); |
| 131 | + defer win.destroy(); |
| 132 | + // window_handle is a positive id assigned by the C layer. |
| 133 | + try std.testing.expect(win.window_handle > 0); |
| 134 | + try std.testing.expect(win.window_handle < webui.WEBUI_MAX_IDS); |
| 135 | + |
| 136 | + // The window was just created; nothing is shown yet. |
| 137 | + try std.testing.expect(!win.isShown()); |
| 138 | +} |
| 139 | + |
| 140 | +test "newWindowWithId rejects 0 and out-of-range ids" { |
| 141 | + try std.testing.expectError(webui.WebUIError.CreateWindowError, webui.newWindowWithId(0)); |
| 142 | + try std.testing.expectError(webui.WebUIError.CreateWindowError, webui.newWindowWithId(webui.WEBUI_MAX_IDS)); |
| 143 | + try std.testing.expectError(webui.WebUIError.CreateWindowError, webui.newWindowWithId(webui.WEBUI_MAX_IDS + 100)); |
| 144 | +} |
| 145 | + |
| 146 | +test "newWindowWithId with explicit id" { |
| 147 | + const id = webui.getNewWindowId(); |
| 148 | + try std.testing.expect(id > 0); |
| 149 | + try std.testing.expect(id < webui.WEBUI_MAX_IDS); |
| 150 | + |
| 151 | + const win = try webui.newWindowWithId(id); |
| 152 | + defer win.destroy(); |
| 153 | + try std.testing.expectEqual(id, win.window_handle); |
| 154 | +} |
| 155 | + |
| 156 | +test "getNewWindowId returns distinct ids" { |
| 157 | + const a = webui.getNewWindowId(); |
| 158 | + const b = webui.getNewWindowId(); |
| 159 | + try std.testing.expect(a != b); |
| 160 | + try std.testing.expect(a > 0); |
| 161 | + try std.testing.expect(b > 0); |
| 162 | +} |
| 163 | + |
| 164 | +test "getFreePort returns a non-zero port" { |
| 165 | + const port = webui.getFreePort(); |
| 166 | + try std.testing.expect(port > 0); |
| 167 | + try std.testing.expect(port < 65536); |
| 168 | +} |
| 169 | + |
| 170 | +test "getMimeType resolves common extensions" { |
| 171 | + try std.testing.expectEqualStrings("text/html", webui.getMimeType("index.html")); |
| 172 | + try std.testing.expectEqualStrings("text/css", webui.getMimeType("style.css")); |
| 173 | + // JavaScript MIME type label varies a bit across versions; just check it's |
| 174 | + // a non-empty string that mentions javascript. |
| 175 | + const js_mime = webui.getMimeType("app.js"); |
| 176 | + try std.testing.expect(js_mime.len > 0); |
| 177 | + try std.testing.expect(std.mem.indexOf(u8, js_mime, "javascript") != null); |
| 178 | +} |
| 179 | + |
| 180 | +test "encode then decode roundtrips" { |
| 181 | + const original: [:0]const u8 = "Hello, WebUI!"; |
| 182 | + const encoded = try webui.encode(original); |
| 183 | + defer webui.free(encoded); |
| 184 | + try std.testing.expect(encoded.len > 0); |
| 185 | + // Base64 of ASCII has no NUL bytes embedded. |
| 186 | + try std.testing.expect(std.mem.indexOfScalar(u8, encoded, 0) == null); |
| 187 | + |
| 188 | + // Build a NUL-terminated copy for the decode call (the C API insists). |
| 189 | + var buf: [128]u8 = undefined; |
| 190 | + @memcpy(buf[0..encoded.len], encoded); |
| 191 | + buf[encoded.len] = 0; |
| 192 | + const encoded_z: [:0]const u8 = buf[0..encoded.len :0]; |
| 193 | + |
| 194 | + const decoded = try webui.decode(encoded_z); |
| 195 | + defer webui.free(decoded); |
| 196 | + try std.testing.expectEqualStrings(original, decoded); |
| 197 | +} |
| 198 | + |
| 199 | +test "malloc / free roundtrip" { |
| 200 | + const buf = try webui.malloc(64); |
| 201 | + defer webui.free(buf); |
| 202 | + try std.testing.expectEqual(@as(usize, 64), buf.len); |
| 203 | + // We can write to the buffer without faulting. |
| 204 | + @memset(buf, 0xAB); |
| 205 | + try std.testing.expectEqual(@as(u8, 0xAB), buf[0]); |
| 206 | + try std.testing.expectEqual(@as(u8, 0xAB), buf[63]); |
| 207 | +} |
| 208 | + |
| 209 | +test "setTimeout / setConfig / setBrowserFolder do not crash" { |
| 210 | + // Pure setters: the most we can check without driving a real browser is |
| 211 | + // that they execute and return. |
| 212 | + webui.setTimeout(0); |
| 213 | + webui.setConfig(.show_wait_connection, true); |
| 214 | + webui.setConfig(.multi_client, false); |
| 215 | + webui.setBrowserFolder(""); |
| 216 | +} |
| 217 | + |
| 218 | +test "setDefaultRootFolder accepts a relative path" { |
| 219 | + // Should at least succeed for the current working directory. |
| 220 | + try webui.setDefaultRootFolder("."); |
| 221 | +} |
| 222 | + |
| 223 | +test "browserExist for NoBrowser returns false" { |
| 224 | + // The "no browser" pseudo-value is never installed; this gives us a |
| 225 | + // deterministic answer without depending on the test host having Chrome. |
| 226 | + try std.testing.expectEqual(false, webui.browserExist(.NoBrowser)); |
| 227 | +} |
| 228 | + |
| 229 | +test "clean is callable" { |
| 230 | + // No assertion: just make sure the symbol is wired up and doesn't crash. |
| 231 | + webui.clean(); |
| 232 | +} |
| 233 | + |
| 234 | +// Suppress "unused" warnings for builtin import on 0.14 paths that don't |
| 235 | +// touch it directly. |
| 236 | +comptime { |
| 237 | + _ = builtin; |
| 238 | +} |
0 commit comments