Skip to content

Commit 06e9057

Browse files
committed
test: add comprehensive unit and smoke tests for webui bindings
- Add src/tests.zig containing verification for enums, constants, error sets, struct layout, and base64 helpers - Add C-backed smoke tests for window, port, mime, memory, and config APIs - Update build to include and wire up test target - Update CI workflow to run tests on every build Increase confidence in correctness and future refactoring by automating regression coverage.
1 parent 3ff6551 commit 06e9057

3 files changed

Lines changed: 290 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ jobs:
3636
- name: Build examples dynamic
3737
if: runner.os != 'Windows'
3838
run: zig build examples -Dis_static=false
39+
- name: Run unit tests
40+
run: zig build test --summary all
3941
lint:
4042
runs-on: ubuntu-latest
4143
steps:

build.zig

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ pub fn build(b: *Build) !void {
9292
.flags_module = flags_module,
9393
.compat_tuple_module = compat_tuple_module,
9494
});
95+
96+
buildTests(b, .{
97+
.optimize = optimize,
98+
.target = target,
99+
.webui_module = webui_module,
100+
.webui_artifact = webui.artifact("webui"),
101+
.flags_module = flags_module,
102+
.compat_tuple_module = compat_tuple_module,
103+
});
95104
}
96105

97106
// ========== Options Structures ==========
@@ -110,6 +119,15 @@ const GenerateDocsOptions = struct {
110119
compat_tuple_module: *Module,
111120
};
112121

122+
const BuildTestsOptions = struct {
123+
optimize: OptimizeMode,
124+
target: Build.ResolvedTarget,
125+
webui_module: *Module,
126+
webui_artifact: *Compile,
127+
flags_module: *Module,
128+
compat_tuple_module: *Module,
129+
};
130+
113131
// ========== Helper Functions ==========
114132

115133
/// Create an object artifact with version compatibility
@@ -166,6 +184,38 @@ fn createExecutable(
166184
}
167185
}
168186

187+
// ========== Tests ==========
188+
189+
fn buildTests(b: *Build, options: BuildTestsOptions) void {
190+
const tests_path = b.path(b.pathJoin(&.{ "src", "tests.zig" }));
191+
192+
const tests = if (builtin.zig_version.minor == 14) b.addTest(.{
193+
.name = "webui-tests",
194+
.root_source_file = tests_path,
195+
.target = options.target,
196+
.optimize = options.optimize,
197+
}) else b.addTest(.{
198+
.name = "webui-tests",
199+
.root_module = b.createModule(.{
200+
.root_source_file = tests_path,
201+
.target = options.target,
202+
.optimize = options.optimize,
203+
}),
204+
});
205+
206+
tests.root_module.addImport("webui", options.webui_module);
207+
tests.root_module.addImport("flags", options.flags_module);
208+
tests.root_module.addImport("compat_tuple", options.compat_tuple_module);
209+
// `linkLibrary` lives on Compile in 0.14/0.15 but was moved entirely to
210+
// Module in 0.16 — go through `root_module` which exists on every
211+
// supported version.
212+
tests.root_module.linkLibrary(options.webui_artifact);
213+
214+
const run_tests = b.addRunArtifact(tests);
215+
const test_step = b.step("test", "Run unit tests");
216+
test_step.dependOn(&run_tests.step);
217+
}
218+
169219
// ========== Documentation Generation ==========
170220

171221
fn generateDocs(b: *Build, options: GenerateDocsOptions) void {

src/tests.zig

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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(&params);
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

Comments
 (0)