feat: zcatgui v0.14.0 - Phase 8 Accessibility & Testing
Accessibility System: - Role enum for all widget types (button, checkbox, slider, tree, table, menu, dialog, etc.) - State packed struct (disabled, focused, selected, checked, expanded, pressed, invalid, readonly, required, busy) - Info struct with label, description, value, position, level, controls, labelled_by - Manager for tracking widget accessibility info - announce() method for screen reader announcements - Live region support (polite, assertive) - Helper constructors for common patterns Testing Framework: - TestRunner for simulating user interactions: - Mouse: click, doubleClick, drag, scroll, moveMouse - Keyboard: pressKey, typeText, shortcut - Time: tick, waitFrames, advanceTime - SnapshotTester for visual regression testing: - capture() to save framebuffer - compare() to diff against baseline - update() to update baseline - Assertions module: - assertVisible, assertContains, assertIntersects - assertColorEqual, assertColorNear - assertInRange Widget count: 35 widgets Test count: 274 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
70fca5177b
commit
1a26d34aa3
4 changed files with 1064 additions and 0 deletions
554
src/core/accessibility.zig
Normal file
554
src/core/accessibility.zig
Normal file
|
|
@ -0,0 +1,554 @@
|
||||||
|
//! Accessibility System
|
||||||
|
//!
|
||||||
|
//! Provides accessibility information for widgets, enabling screen readers
|
||||||
|
//! and other assistive technologies to understand the UI.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Accessibility role for widgets
|
||||||
|
pub const Role = enum {
|
||||||
|
/// No specific role
|
||||||
|
none,
|
||||||
|
/// Push button
|
||||||
|
button,
|
||||||
|
/// Checkbox (can be checked/unchecked)
|
||||||
|
checkbox,
|
||||||
|
/// Radio button (mutually exclusive selection)
|
||||||
|
radio,
|
||||||
|
/// Text input field
|
||||||
|
textbox,
|
||||||
|
/// Multi-line text editor
|
||||||
|
editor,
|
||||||
|
/// Slider/trackbar
|
||||||
|
slider,
|
||||||
|
/// Progress indicator
|
||||||
|
progressbar,
|
||||||
|
/// Dropdown list
|
||||||
|
combobox,
|
||||||
|
/// List box
|
||||||
|
listbox,
|
||||||
|
/// List item
|
||||||
|
listitem,
|
||||||
|
/// Tree view
|
||||||
|
tree,
|
||||||
|
/// Tree item
|
||||||
|
treeitem,
|
||||||
|
/// Table/grid
|
||||||
|
table,
|
||||||
|
/// Table row
|
||||||
|
row,
|
||||||
|
/// Table cell
|
||||||
|
cell,
|
||||||
|
/// Table header
|
||||||
|
columnheader,
|
||||||
|
/// Tab list container
|
||||||
|
tablist,
|
||||||
|
/// Individual tab
|
||||||
|
tab,
|
||||||
|
/// Tab content panel
|
||||||
|
tabpanel,
|
||||||
|
/// Menu bar
|
||||||
|
menubar,
|
||||||
|
/// Menu
|
||||||
|
menu,
|
||||||
|
/// Menu item
|
||||||
|
menuitem,
|
||||||
|
/// Dialog/modal
|
||||||
|
dialog,
|
||||||
|
/// Alert dialog
|
||||||
|
alertdialog,
|
||||||
|
/// Tooltip
|
||||||
|
tooltip,
|
||||||
|
/// Status message
|
||||||
|
status,
|
||||||
|
/// Link
|
||||||
|
link,
|
||||||
|
/// Image
|
||||||
|
img,
|
||||||
|
/// Group of related elements
|
||||||
|
group,
|
||||||
|
/// Panel/container
|
||||||
|
region,
|
||||||
|
/// Form
|
||||||
|
form,
|
||||||
|
/// Search field
|
||||||
|
search,
|
||||||
|
/// Separator
|
||||||
|
separator,
|
||||||
|
/// Scrollbar
|
||||||
|
scrollbar,
|
||||||
|
/// Spinner/loading indicator
|
||||||
|
spinbutton,
|
||||||
|
/// Static text
|
||||||
|
label,
|
||||||
|
|
||||||
|
/// Get human-readable name for the role
|
||||||
|
pub fn name(self: Role) []const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.none => "none",
|
||||||
|
.button => "button",
|
||||||
|
.checkbox => "checkbox",
|
||||||
|
.radio => "radio button",
|
||||||
|
.textbox => "text box",
|
||||||
|
.editor => "text editor",
|
||||||
|
.slider => "slider",
|
||||||
|
.progressbar => "progress bar",
|
||||||
|
.combobox => "combo box",
|
||||||
|
.listbox => "list box",
|
||||||
|
.listitem => "list item",
|
||||||
|
.tree => "tree",
|
||||||
|
.treeitem => "tree item",
|
||||||
|
.table => "table",
|
||||||
|
.row => "row",
|
||||||
|
.cell => "cell",
|
||||||
|
.columnheader => "column header",
|
||||||
|
.tablist => "tab list",
|
||||||
|
.tab => "tab",
|
||||||
|
.tabpanel => "tab panel",
|
||||||
|
.menubar => "menu bar",
|
||||||
|
.menu => "menu",
|
||||||
|
.menuitem => "menu item",
|
||||||
|
.dialog => "dialog",
|
||||||
|
.alertdialog => "alert dialog",
|
||||||
|
.tooltip => "tooltip",
|
||||||
|
.status => "status",
|
||||||
|
.link => "link",
|
||||||
|
.img => "image",
|
||||||
|
.group => "group",
|
||||||
|
.region => "region",
|
||||||
|
.form => "form",
|
||||||
|
.search => "search",
|
||||||
|
.separator => "separator",
|
||||||
|
.scrollbar => "scrollbar",
|
||||||
|
.spinbutton => "spin button",
|
||||||
|
.label => "label",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Accessibility state flags
|
||||||
|
pub const State = packed struct {
|
||||||
|
/// Widget is disabled
|
||||||
|
disabled: bool = false,
|
||||||
|
/// Widget is hidden
|
||||||
|
hidden: bool = false,
|
||||||
|
/// Widget has keyboard focus
|
||||||
|
focused: bool = false,
|
||||||
|
/// Widget is selected
|
||||||
|
selected: bool = false,
|
||||||
|
/// Checkbox/toggle is checked
|
||||||
|
checked: bool = false,
|
||||||
|
/// Node is expanded (tree, menu)
|
||||||
|
expanded: bool = false,
|
||||||
|
/// Widget is pressed (button)
|
||||||
|
pressed: bool = false,
|
||||||
|
/// Widget has invalid input
|
||||||
|
invalid: bool = false,
|
||||||
|
/// Widget is read-only
|
||||||
|
readonly: bool = false,
|
||||||
|
/// Widget is required
|
||||||
|
required: bool = false,
|
||||||
|
/// Content is being loaded
|
||||||
|
busy: bool = false,
|
||||||
|
/// Widget supports multiple selection
|
||||||
|
multiselectable: bool = false,
|
||||||
|
/// Item is draggable
|
||||||
|
draggable: bool = false,
|
||||||
|
_padding: u3 = 0,
|
||||||
|
|
||||||
|
pub fn default() State {
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Accessibility information for a widget
|
||||||
|
pub const Info = struct {
|
||||||
|
/// Widget's role
|
||||||
|
role: Role = .none,
|
||||||
|
/// Accessible label (name)
|
||||||
|
label: []const u8 = "",
|
||||||
|
/// Detailed description
|
||||||
|
description: []const u8 = "",
|
||||||
|
/// Current value (for sliders, progress, etc.)
|
||||||
|
value: ?[]const u8 = null,
|
||||||
|
/// Minimum value (for range widgets)
|
||||||
|
value_min: ?f64 = null,
|
||||||
|
/// Maximum value (for range widgets)
|
||||||
|
value_max: ?f64 = null,
|
||||||
|
/// Current value as number
|
||||||
|
value_now: ?f64 = null,
|
||||||
|
/// State flags
|
||||||
|
state: State = .{},
|
||||||
|
/// Position in set (e.g., "3 of 10")
|
||||||
|
pos_in_set: ?u32 = null,
|
||||||
|
/// Size of set
|
||||||
|
set_size: ?u32 = null,
|
||||||
|
/// Level (for headings, tree items)
|
||||||
|
level: ?u8 = null,
|
||||||
|
/// ID of element this controls
|
||||||
|
controls: ?u64 = null,
|
||||||
|
/// ID of element that labels this
|
||||||
|
labelled_by: ?u64 = null,
|
||||||
|
/// ID of element that describes this
|
||||||
|
described_by: ?u64 = null,
|
||||||
|
/// Error message (when invalid)
|
||||||
|
error_message: []const u8 = "",
|
||||||
|
/// Keyboard shortcut
|
||||||
|
shortcut: []const u8 = "",
|
||||||
|
/// Live region type
|
||||||
|
live: LiveRegion = .off,
|
||||||
|
/// Auto-complete type
|
||||||
|
autocomplete: AutoComplete = .none,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Create info for a button
|
||||||
|
pub fn button(label_text: []const u8) Self {
|
||||||
|
return .{
|
||||||
|
.role = .button,
|
||||||
|
.label = label_text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a checkbox
|
||||||
|
pub fn checkbox(label_text: []const u8, is_checked: bool) Self {
|
||||||
|
return .{
|
||||||
|
.role = .checkbox,
|
||||||
|
.label = label_text,
|
||||||
|
.state = .{ .checked = is_checked },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a text input
|
||||||
|
pub fn textInput(label_text: []const u8, placeholder: []const u8) Self {
|
||||||
|
return .{
|
||||||
|
.role = .textbox,
|
||||||
|
.label = label_text,
|
||||||
|
.description = placeholder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a slider
|
||||||
|
pub fn slider(label_text: []const u8, min: f64, max: f64, current: f64) Self {
|
||||||
|
return .{
|
||||||
|
.role = .slider,
|
||||||
|
.label = label_text,
|
||||||
|
.value_min = min,
|
||||||
|
.value_max = max,
|
||||||
|
.value_now = current,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a progress bar
|
||||||
|
pub fn progressBar(label_text: []const u8, progress: f64) Self {
|
||||||
|
return .{
|
||||||
|
.role = .progressbar,
|
||||||
|
.label = label_text,
|
||||||
|
.value_min = 0,
|
||||||
|
.value_max = 100,
|
||||||
|
.value_now = progress * 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a list item
|
||||||
|
pub fn listItem(label_text: []const u8, position: u32, total: u32) Self {
|
||||||
|
return .{
|
||||||
|
.role = .listitem,
|
||||||
|
.label = label_text,
|
||||||
|
.pos_in_set = position,
|
||||||
|
.set_size = total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a tree item
|
||||||
|
pub fn treeItem(label_text: []const u8, lvl: u8, is_expanded: bool) Self {
|
||||||
|
return .{
|
||||||
|
.role = .treeitem,
|
||||||
|
.label = label_text,
|
||||||
|
.level = lvl,
|
||||||
|
.state = .{ .expanded = is_expanded },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a dialog
|
||||||
|
pub fn dialog(title: []const u8, is_modal: bool) Self {
|
||||||
|
_ = is_modal;
|
||||||
|
return .{
|
||||||
|
.role = .dialog,
|
||||||
|
.label = title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create info for a menu item
|
||||||
|
pub fn menuItem(label_text: []const u8, has_shortcut: []const u8) Self {
|
||||||
|
return .{
|
||||||
|
.role = .menuitem,
|
||||||
|
.label = label_text,
|
||||||
|
.shortcut = has_shortcut,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set disabled state
|
||||||
|
pub fn setDisabled(self: *Self, disabled: bool) void {
|
||||||
|
self.state.disabled = disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set focused state
|
||||||
|
pub fn setFocused(self: *Self, focused: bool) void {
|
||||||
|
self.state.focused = focused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set selected state
|
||||||
|
pub fn setSelected(self: *Self, selected: bool) void {
|
||||||
|
self.state.selected = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set expanded state
|
||||||
|
pub fn setExpanded(self: *Self, expanded: bool) void {
|
||||||
|
self.state.expanded = expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format as accessible text announcement
|
||||||
|
pub fn announce(self: Self, buf: []u8) []const u8 {
|
||||||
|
var stream = std.io.fixedBufferStream(buf);
|
||||||
|
const writer = stream.writer();
|
||||||
|
|
||||||
|
// Label
|
||||||
|
if (self.label.len > 0) {
|
||||||
|
writer.writeAll(self.label) catch return buf[0..0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role
|
||||||
|
writer.print(", {s}", .{self.role.name()}) catch return buf[0..stream.pos];
|
||||||
|
|
||||||
|
// Value
|
||||||
|
if (self.value) |v| {
|
||||||
|
writer.print(", {s}", .{v}) catch return buf[0..stream.pos];
|
||||||
|
} else if (self.value_now) |v| {
|
||||||
|
if (self.value_min != null and self.value_max != null) {
|
||||||
|
writer.print(", {d:.0} of {d:.0}", .{ v, self.value_max.? }) catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State
|
||||||
|
if (self.state.disabled) {
|
||||||
|
writer.writeAll(", disabled") catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
if (self.state.checked) {
|
||||||
|
writer.writeAll(", checked") catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
if (self.state.expanded) {
|
||||||
|
writer.writeAll(", expanded") catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
if (self.state.selected) {
|
||||||
|
writer.writeAll(", selected") catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
if (self.state.invalid) {
|
||||||
|
writer.writeAll(", invalid") catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position
|
||||||
|
if (self.pos_in_set != null and self.set_size != null) {
|
||||||
|
writer.print(", {d} of {d}", .{ self.pos_in_set.?, self.set_size.? }) catch return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[0..stream.pos];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Live region types for dynamic content
|
||||||
|
pub const LiveRegion = enum {
|
||||||
|
/// Not a live region
|
||||||
|
off,
|
||||||
|
/// Polite - announce when convenient
|
||||||
|
polite,
|
||||||
|
/// Assertive - announce immediately
|
||||||
|
assertive,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Auto-complete types
|
||||||
|
pub const AutoComplete = enum {
|
||||||
|
/// No auto-complete
|
||||||
|
none,
|
||||||
|
/// Inline completion
|
||||||
|
@"inline",
|
||||||
|
/// List of suggestions
|
||||||
|
list,
|
||||||
|
/// Both inline and list
|
||||||
|
both,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Accessibility manager for tracking widget info
|
||||||
|
pub const Manager = struct {
|
||||||
|
/// Map of widget IDs to accessibility info
|
||||||
|
info: std.AutoHashMap(u64, Info),
|
||||||
|
/// Current focus ID
|
||||||
|
focus_id: ?u64 = null,
|
||||||
|
/// Pending announcements
|
||||||
|
announcements: [8]Announcement = undefined,
|
||||||
|
announcement_count: usize = 0,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.info = std.AutoHashMap(u64, Info).init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.info.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register accessibility info for a widget
|
||||||
|
pub fn register(self: *Self, widget_id: u64, acc_info: Info) void {
|
||||||
|
self.info.put(widget_id, acc_info) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unregister a widget
|
||||||
|
pub fn unregister(self: *Self, widget_id: u64) void {
|
||||||
|
_ = self.info.remove(widget_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get info for a widget
|
||||||
|
pub fn get(self: Self, widget_id: u64) ?Info {
|
||||||
|
return self.info.get(widget_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update focus
|
||||||
|
pub fn setFocus(self: *Self, widget_id: u64) void {
|
||||||
|
// Clear old focus
|
||||||
|
if (self.focus_id) |old_id| {
|
||||||
|
if (self.info.getPtr(old_id)) |info| {
|
||||||
|
info.state.focused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new focus
|
||||||
|
self.focus_id = widget_id;
|
||||||
|
if (self.info.getPtr(widget_id)) |info| {
|
||||||
|
info.state.focused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue an announcement
|
||||||
|
pub fn queueAnnouncement(self: *Self, text: []const u8, priority: LiveRegion) void {
|
||||||
|
if (self.announcement_count < 8) {
|
||||||
|
self.announcements[self.announcement_count] = .{
|
||||||
|
.text = text,
|
||||||
|
.priority = priority,
|
||||||
|
};
|
||||||
|
self.announcement_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get and clear pending announcements
|
||||||
|
pub fn getAnnouncements(self: *Self) []const Announcement {
|
||||||
|
const count = self.announcement_count;
|
||||||
|
self.announcement_count = 0;
|
||||||
|
return self.announcements[0..count];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all info
|
||||||
|
pub fn clear(self: *Self) void {
|
||||||
|
self.info.clearRetainingCapacity();
|
||||||
|
self.focus_id = null;
|
||||||
|
self.announcement_count = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Pending announcement
|
||||||
|
pub const Announcement = struct {
|
||||||
|
text: []const u8,
|
||||||
|
priority: LiveRegion,
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "Role name" {
|
||||||
|
try std.testing.expectEqualStrings("button", Role.button.name());
|
||||||
|
try std.testing.expectEqualStrings("checkbox", Role.checkbox.name());
|
||||||
|
try std.testing.expectEqualStrings("text box", Role.textbox.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "State packed size" {
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), @sizeOf(State));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Info button" {
|
||||||
|
const info = Info.button("Submit");
|
||||||
|
try std.testing.expectEqual(Role.button, info.role);
|
||||||
|
try std.testing.expectEqualStrings("Submit", info.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Info checkbox" {
|
||||||
|
const info = Info.checkbox("Accept terms", true);
|
||||||
|
try std.testing.expectEqual(Role.checkbox, info.role);
|
||||||
|
try std.testing.expect(info.state.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Info slider" {
|
||||||
|
const info = Info.slider("Volume", 0, 100, 50);
|
||||||
|
try std.testing.expectEqual(Role.slider, info.role);
|
||||||
|
try std.testing.expectEqual(@as(?f64, 0), info.value_min);
|
||||||
|
try std.testing.expectEqual(@as(?f64, 100), info.value_max);
|
||||||
|
try std.testing.expectEqual(@as(?f64, 50), info.value_now);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Info announce" {
|
||||||
|
var buf: [256]u8 = undefined;
|
||||||
|
const info = Info.checkbox("Accept terms", true);
|
||||||
|
const text = info.announce(&buf);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, text, "Accept terms") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, text, "checkbox") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, text, "checked") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Manager basic" {
|
||||||
|
var manager = Manager.init(std.testing.allocator);
|
||||||
|
defer manager.deinit();
|
||||||
|
|
||||||
|
manager.register(1, Info.button("Test"));
|
||||||
|
const info = manager.get(1);
|
||||||
|
try std.testing.expect(info != null);
|
||||||
|
try std.testing.expectEqualStrings("Test", info.?.label);
|
||||||
|
|
||||||
|
manager.unregister(1);
|
||||||
|
try std.testing.expect(manager.get(1) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Manager focus" {
|
||||||
|
var manager = Manager.init(std.testing.allocator);
|
||||||
|
defer manager.deinit();
|
||||||
|
|
||||||
|
manager.register(1, Info.button("Button 1"));
|
||||||
|
manager.register(2, Info.button("Button 2"));
|
||||||
|
|
||||||
|
manager.setFocus(1);
|
||||||
|
try std.testing.expectEqual(@as(?u64, 1), manager.focus_id);
|
||||||
|
|
||||||
|
if (manager.get(1)) |info| {
|
||||||
|
try std.testing.expect(info.state.focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.setFocus(2);
|
||||||
|
try std.testing.expectEqual(@as(?u64, 2), manager.focus_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Manager announcements" {
|
||||||
|
var manager = Manager.init(std.testing.allocator);
|
||||||
|
defer manager.deinit();
|
||||||
|
|
||||||
|
manager.queueAnnouncement("File saved", .polite);
|
||||||
|
manager.queueAnnouncement("Error occurred", .assertive);
|
||||||
|
|
||||||
|
const announcements = manager.getAnnouncements();
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), announcements.len);
|
||||||
|
try std.testing.expectEqualStrings("File saved", announcements[0].text);
|
||||||
|
try std.testing.expectEqualStrings("Error occurred", announcements[1].text);
|
||||||
|
|
||||||
|
// Should be cleared
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), manager.getAnnouncements().len);
|
||||||
|
}
|
||||||
497
src/utils/testing.zig
Normal file
497
src/utils/testing.zig
Normal file
|
|
@ -0,0 +1,497 @@
|
||||||
|
//! Testing Framework
|
||||||
|
//!
|
||||||
|
//! Provides tools for testing zcatgui widgets and applications.
|
||||||
|
//! Includes simulated input, widget finding, and assertions.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Context = @import("../core/context.zig").Context;
|
||||||
|
const Input = @import("../core/input.zig");
|
||||||
|
const Layout = @import("../core/layout.zig");
|
||||||
|
const Style = @import("../core/style.zig");
|
||||||
|
const Framebuffer = @import("../render/framebuffer.zig").Framebuffer;
|
||||||
|
|
||||||
|
/// Test runner for simulating user interactions
|
||||||
|
pub const TestRunner = struct {
|
||||||
|
/// Simulated input state
|
||||||
|
input: Input.InputState,
|
||||||
|
/// Current time in milliseconds
|
||||||
|
current_time_ms: i64 = 0,
|
||||||
|
/// Frame count
|
||||||
|
frame_count: u64 = 0,
|
||||||
|
/// Last action result
|
||||||
|
last_result: ?ActionResult = null,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Initialize a new test runner
|
||||||
|
pub fn init() Self {
|
||||||
|
return .{
|
||||||
|
.input = Input.InputState{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the test runner state
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.input = Input.InputState{};
|
||||||
|
self.current_time_ms = 0;
|
||||||
|
self.frame_count = 0;
|
||||||
|
self.last_result = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance time by milliseconds
|
||||||
|
pub fn advanceTime(self: *Self, ms: i64) void {
|
||||||
|
self.current_time_ms += ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a frame update
|
||||||
|
pub fn tick(self: *Self) void {
|
||||||
|
self.frame_count += 1;
|
||||||
|
self.advanceTime(16); // ~60fps
|
||||||
|
self.input.endFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for specified number of frames
|
||||||
|
pub fn waitFrames(self: *Self, count: u32) void {
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < count) : (i += 1) {
|
||||||
|
self.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate mouse move
|
||||||
|
pub fn moveMouse(self: *Self, x: i32, y: i32) void {
|
||||||
|
self.input.mouse_x = x;
|
||||||
|
self.input.mouse_y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate mouse click at position
|
||||||
|
pub fn click(self: *Self, x: i32, y: i32) void {
|
||||||
|
self.moveMouse(x, y);
|
||||||
|
self.input.setMouseButton(.left, true);
|
||||||
|
self.tick();
|
||||||
|
self.input.setMouseButton(.left, false);
|
||||||
|
self.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate mouse press (without release)
|
||||||
|
pub fn mouseDown(self: *Self, x: i32, y: i32) void {
|
||||||
|
self.moveMouse(x, y);
|
||||||
|
self.input.setMouseButton(.left, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate mouse release
|
||||||
|
pub fn mouseUp(self: *Self) void {
|
||||||
|
self.input.setMouseButton(.left, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate double click
|
||||||
|
pub fn doubleClick(self: *Self, x: i32, y: i32) void {
|
||||||
|
self.click(x, y);
|
||||||
|
self.advanceTime(100); // Short delay
|
||||||
|
self.click(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate mouse drag
|
||||||
|
pub fn drag(self: *Self, start_x: i32, start_y: i32, end_x: i32, end_y: i32, steps: u32) void {
|
||||||
|
self.mouseDown(start_x, start_y);
|
||||||
|
self.tick();
|
||||||
|
|
||||||
|
const actual_steps = if (steps == 0) 10 else steps;
|
||||||
|
var i: u32 = 1;
|
||||||
|
while (i <= actual_steps) : (i += 1) {
|
||||||
|
const t = @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(actual_steps));
|
||||||
|
const x = start_x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(end_x - start_x)) * t));
|
||||||
|
const y = start_y + @as(i32, @intFromFloat(@as(f32, @floatFromInt(end_y - start_y)) * t));
|
||||||
|
self.moveMouse(x, y);
|
||||||
|
self.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mouseUp();
|
||||||
|
self.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate scroll
|
||||||
|
pub fn scroll(self: *Self, x: i32, y: i32, delta_y: i32) void {
|
||||||
|
self.moveMouse(x, y);
|
||||||
|
self.input.scroll_y = delta_y;
|
||||||
|
self.tick();
|
||||||
|
self.input.scroll_y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate key press
|
||||||
|
pub fn pressKey(self: *Self, key: Input.Key) void {
|
||||||
|
self.input.setKeyState(key, true);
|
||||||
|
self.tick();
|
||||||
|
self.input.setKeyState(key, false);
|
||||||
|
self.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate key down (without release)
|
||||||
|
pub fn keyDown(self: *Self, key: Input.Key) void {
|
||||||
|
self.input.setKeyState(key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate key up
|
||||||
|
pub fn keyUp(self: *Self, key: Input.Key) void {
|
||||||
|
self.input.setKeyState(key, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate typing text
|
||||||
|
pub fn typeText(self: *Self, text: []const u8) void {
|
||||||
|
for (text) |char| {
|
||||||
|
if (charToKey(char)) |key| {
|
||||||
|
// Check if shift is needed
|
||||||
|
const needs_shift = std.ascii.isUpper(char);
|
||||||
|
if (needs_shift) {
|
||||||
|
self.keyDown(.left_shift);
|
||||||
|
}
|
||||||
|
self.pressKey(key);
|
||||||
|
if (needs_shift) {
|
||||||
|
self.keyUp(.left_shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate keyboard shortcut (e.g., Ctrl+S)
|
||||||
|
pub fn shortcut(self: *Self, key: Input.Key, ctrl: bool, shift: bool, alt: bool) void {
|
||||||
|
if (ctrl) self.keyDown(.left_ctrl);
|
||||||
|
if (shift) self.keyDown(.left_shift);
|
||||||
|
if (alt) self.keyDown(.left_alt);
|
||||||
|
|
||||||
|
self.pressKey(key);
|
||||||
|
|
||||||
|
if (alt) self.keyUp(.left_alt);
|
||||||
|
if (shift) self.keyUp(.left_shift);
|
||||||
|
if (ctrl) self.keyUp(.left_ctrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press Enter key
|
||||||
|
pub fn pressEnter(self: *Self) void {
|
||||||
|
self.pressKey(.enter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press Tab key
|
||||||
|
pub fn pressTab(self: *Self) void {
|
||||||
|
self.pressKey(.tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Press Escape key
|
||||||
|
pub fn pressEscape(self: *Self) void {
|
||||||
|
self.pressKey(.escape);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the simulated input state
|
||||||
|
pub fn getInput(self: *Self) *Input.InputState {
|
||||||
|
return &self.input;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Result of a test action
|
||||||
|
pub const ActionResult = struct {
|
||||||
|
success: bool,
|
||||||
|
message: []const u8 = "",
|
||||||
|
widget_id: ?u64 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Convert ASCII char to key
|
||||||
|
fn charToKey(char: u8) ?Input.Key {
|
||||||
|
return switch (char) {
|
||||||
|
'a', 'A' => .a,
|
||||||
|
'b', 'B' => .b,
|
||||||
|
'c', 'C' => .c,
|
||||||
|
'd', 'D' => .d,
|
||||||
|
'e', 'E' => .e,
|
||||||
|
'f', 'F' => .f,
|
||||||
|
'g', 'G' => .g,
|
||||||
|
'h', 'H' => .h,
|
||||||
|
'i', 'I' => .i,
|
||||||
|
'j', 'J' => .j,
|
||||||
|
'k', 'K' => .k,
|
||||||
|
'l', 'L' => .l,
|
||||||
|
'm', 'M' => .m,
|
||||||
|
'n', 'N' => .n,
|
||||||
|
'o', 'O' => .o,
|
||||||
|
'p', 'P' => .p,
|
||||||
|
'q', 'Q' => .q,
|
||||||
|
'r', 'R' => .r,
|
||||||
|
's', 'S' => .s,
|
||||||
|
't', 'T' => .t,
|
||||||
|
'u', 'U' => .u,
|
||||||
|
'v', 'V' => .v,
|
||||||
|
'w', 'W' => .w,
|
||||||
|
'x', 'X' => .x,
|
||||||
|
'y', 'Y' => .y,
|
||||||
|
'z', 'Z' => .z,
|
||||||
|
'0' => .@"0",
|
||||||
|
'1' => .@"1",
|
||||||
|
'2' => .@"2",
|
||||||
|
'3' => .@"3",
|
||||||
|
'4' => .@"4",
|
||||||
|
'5' => .@"5",
|
||||||
|
'6' => .@"6",
|
||||||
|
'7' => .@"7",
|
||||||
|
'8' => .@"8",
|
||||||
|
'9' => .@"9",
|
||||||
|
' ' => .space,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot testing for visual regression
|
||||||
|
pub const SnapshotTester = struct {
|
||||||
|
/// Directory for storing snapshots
|
||||||
|
snapshot_dir: []const u8,
|
||||||
|
/// Allocator
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
/// Tolerance for pixel comparison (0-255)
|
||||||
|
tolerance: u8 = 0,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Initialize snapshot tester
|
||||||
|
pub fn init(allocator: std.mem.Allocator, dir: []const u8) Self {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.snapshot_dir = dir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capture a snapshot
|
||||||
|
pub fn capture(self: Self, fb: *const Framebuffer, name: []const u8) !void {
|
||||||
|
const path = try std.fmt.allocPrint(self.allocator, "{s}/{s}.raw", .{ self.snapshot_dir, name });
|
||||||
|
defer self.allocator.free(path);
|
||||||
|
|
||||||
|
const file = try std.fs.cwd().createFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
// Write dimensions
|
||||||
|
var header: [8]u8 = undefined;
|
||||||
|
std.mem.writeInt(u32, header[0..4], fb.width, .little);
|
||||||
|
std.mem.writeInt(u32, header[4..8], fb.height, .little);
|
||||||
|
try file.writeAll(&header);
|
||||||
|
|
||||||
|
// Write pixel data
|
||||||
|
try file.writeAll(fb.pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare framebuffer to stored snapshot
|
||||||
|
pub fn compare(self: Self, fb: *const Framebuffer, name: []const u8) !CompareResult {
|
||||||
|
const path = try std.fmt.allocPrint(self.allocator, "{s}/{s}.raw", .{ self.snapshot_dir, name });
|
||||||
|
defer self.allocator.free(path);
|
||||||
|
|
||||||
|
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
|
||||||
|
if (err == error.FileNotFound) {
|
||||||
|
return .{ .status = .missing };
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
// Read header
|
||||||
|
var header: [8]u8 = undefined;
|
||||||
|
_ = try file.readAll(&header);
|
||||||
|
const stored_width = std.mem.readInt(u32, header[0..4], .little);
|
||||||
|
const stored_height = std.mem.readInt(u32, header[4..8], .little);
|
||||||
|
|
||||||
|
// Check dimensions
|
||||||
|
if (stored_width != fb.width or stored_height != fb.height) {
|
||||||
|
return .{
|
||||||
|
.status = .size_mismatch,
|
||||||
|
.expected_width = stored_width,
|
||||||
|
.expected_height = stored_height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and compare pixels
|
||||||
|
const stored_pixels = try self.allocator.alloc(u8, fb.pixels.len);
|
||||||
|
defer self.allocator.free(stored_pixels);
|
||||||
|
_ = try file.readAll(stored_pixels);
|
||||||
|
|
||||||
|
var diff_count: u64 = 0;
|
||||||
|
var max_diff: u8 = 0;
|
||||||
|
var first_diff_x: ?u32 = null;
|
||||||
|
var first_diff_y: ?u32 = null;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < fb.pixels.len) : (i += 1) {
|
||||||
|
const diff = if (fb.pixels[i] > stored_pixels[i])
|
||||||
|
fb.pixels[i] - stored_pixels[i]
|
||||||
|
else
|
||||||
|
stored_pixels[i] - fb.pixels[i];
|
||||||
|
|
||||||
|
if (diff > self.tolerance) {
|
||||||
|
diff_count += 1;
|
||||||
|
if (diff > max_diff) max_diff = diff;
|
||||||
|
if (first_diff_x == null) {
|
||||||
|
const pixel_idx = i / 4;
|
||||||
|
first_diff_x = @intCast(pixel_idx % fb.width);
|
||||||
|
first_diff_y = @intCast(pixel_idx / fb.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff_count == 0) {
|
||||||
|
return .{ .status = .match };
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.status = .mismatch,
|
||||||
|
.diff_pixels = diff_count / 4,
|
||||||
|
.max_diff = max_diff,
|
||||||
|
.first_diff_x = first_diff_x,
|
||||||
|
.first_diff_y = first_diff_y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update snapshot (overwrite with current)
|
||||||
|
pub fn update(self: Self, fb: *const Framebuffer, name: []const u8) !void {
|
||||||
|
try self.capture(fb, name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Result of snapshot comparison
|
||||||
|
pub const CompareResult = struct {
|
||||||
|
status: Status,
|
||||||
|
diff_pixels: u64 = 0,
|
||||||
|
max_diff: u8 = 0,
|
||||||
|
first_diff_x: ?u32 = null,
|
||||||
|
first_diff_y: ?u32 = null,
|
||||||
|
expected_width: u32 = 0,
|
||||||
|
expected_height: u32 = 0,
|
||||||
|
|
||||||
|
pub const Status = enum {
|
||||||
|
match,
|
||||||
|
mismatch,
|
||||||
|
missing,
|
||||||
|
size_mismatch,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn isMatch(self: CompareResult) bool {
|
||||||
|
return self.status == .match;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Test assertions for widgets
|
||||||
|
pub const Assertions = struct {
|
||||||
|
/// Assert a rect is visible (non-empty)
|
||||||
|
pub fn assertVisible(rect: Layout.Rect) !void {
|
||||||
|
if (rect.isEmpty()) {
|
||||||
|
return error.AssertionFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert rect contains point
|
||||||
|
pub fn assertContains(rect: Layout.Rect, x: i32, y: i32) !void {
|
||||||
|
if (!rect.contains(x, y)) {
|
||||||
|
return error.AssertionFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert rects intersect
|
||||||
|
pub fn assertIntersects(a: Layout.Rect, b: Layout.Rect) !void {
|
||||||
|
if (!a.intersects(b)) {
|
||||||
|
return error.AssertionFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert colors are equal
|
||||||
|
pub fn assertColorEqual(a: Style.Color, b: Style.Color) !void {
|
||||||
|
if (a.r != b.r or a.g != b.g or a.b != b.b or a.a != b.a) {
|
||||||
|
return error.AssertionFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert colors are approximately equal (within tolerance)
|
||||||
|
pub fn assertColorNear(a: Style.Color, b: Style.Color, tolerance: u8) !void {
|
||||||
|
const dr = if (a.r > b.r) a.r - b.r else b.r - a.r;
|
||||||
|
const dg = if (a.g > b.g) a.g - b.g else b.g - a.g;
|
||||||
|
const db = if (a.b > b.b) a.b - b.b else b.b - a.b;
|
||||||
|
const da = if (a.a > b.a) a.a - b.a else b.a - a.a;
|
||||||
|
|
||||||
|
if (dr > tolerance or dg > tolerance or db > tolerance or da > tolerance) {
|
||||||
|
return error.AssertionFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert value is within range
|
||||||
|
pub fn assertInRange(value: anytype, min: @TypeOf(value), max: @TypeOf(value)) !void {
|
||||||
|
if (value < min or value > max) {
|
||||||
|
return error.AssertionFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "TestRunner init" {
|
||||||
|
const runner = TestRunner.init();
|
||||||
|
try std.testing.expectEqual(@as(i64, 0), runner.current_time_ms);
|
||||||
|
try std.testing.expectEqual(@as(u64, 0), runner.frame_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TestRunner tick" {
|
||||||
|
var runner = TestRunner.init();
|
||||||
|
runner.tick();
|
||||||
|
try std.testing.expectEqual(@as(u64, 1), runner.frame_count);
|
||||||
|
try std.testing.expectEqual(@as(i64, 16), runner.current_time_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TestRunner mouse" {
|
||||||
|
var runner = TestRunner.init();
|
||||||
|
|
||||||
|
runner.moveMouse(100, 200);
|
||||||
|
try std.testing.expectEqual(@as(i32, 100), runner.input.mouse_x);
|
||||||
|
try std.testing.expectEqual(@as(i32, 200), runner.input.mouse_y);
|
||||||
|
|
||||||
|
runner.click(150, 250);
|
||||||
|
try std.testing.expectEqual(@as(i32, 150), runner.input.mouse_x);
|
||||||
|
try std.testing.expectEqual(@as(i32, 250), runner.input.mouse_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TestRunner keyboard" {
|
||||||
|
var runner = TestRunner.init();
|
||||||
|
runner.pressKey(.a);
|
||||||
|
try std.testing.expectEqual(@as(u64, 2), runner.frame_count); // Press + release
|
||||||
|
}
|
||||||
|
|
||||||
|
test "TestRunner shortcut" {
|
||||||
|
var runner = TestRunner.init();
|
||||||
|
runner.shortcut(.s, true, false, false); // Ctrl+S
|
||||||
|
// Should have pressed and released the keys
|
||||||
|
try std.testing.expect(runner.frame_count >= 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "charToKey" {
|
||||||
|
try std.testing.expectEqual(Input.Key.a, charToKey('a').?);
|
||||||
|
try std.testing.expectEqual(Input.Key.a, charToKey('A').?);
|
||||||
|
try std.testing.expectEqual(Input.Key.space, charToKey(' ').?);
|
||||||
|
try std.testing.expect(charToKey('!') == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Assertions visible" {
|
||||||
|
const visible = Layout.Rect{ .x = 0, .y = 0, .w = 100, .h = 100 };
|
||||||
|
const empty = Layout.Rect{ .x = 0, .y = 0, .w = 0, .h = 0 };
|
||||||
|
|
||||||
|
try Assertions.assertVisible(visible);
|
||||||
|
try std.testing.expectError(error.AssertionFailed, Assertions.assertVisible(empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Assertions contains" {
|
||||||
|
const rect = Layout.Rect{ .x = 0, .y = 0, .w = 100, .h = 100 };
|
||||||
|
|
||||||
|
try Assertions.assertContains(rect, 50, 50);
|
||||||
|
try std.testing.expectError(error.AssertionFailed, Assertions.assertContains(rect, 150, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Assertions color" {
|
||||||
|
const red = Style.Color.rgba(255, 0, 0, 255);
|
||||||
|
const also_red = Style.Color.rgba(255, 0, 0, 255);
|
||||||
|
const almost_red = Style.Color.rgba(250, 5, 0, 255);
|
||||||
|
|
||||||
|
try Assertions.assertColorEqual(red, also_red);
|
||||||
|
try Assertions.assertColorNear(red, almost_red, 10);
|
||||||
|
try std.testing.expectError(error.AssertionFailed, Assertions.assertColorEqual(red, almost_red));
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
pub const arena = @import("arena.zig");
|
pub const arena = @import("arena.zig");
|
||||||
pub const pool = @import("pool.zig");
|
pub const pool = @import("pool.zig");
|
||||||
pub const benchmark = @import("benchmark.zig");
|
pub const benchmark = @import("benchmark.zig");
|
||||||
|
pub const testing_utils = @import("testing.zig");
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub const FrameArena = arena.FrameArena;
|
pub const FrameArena = arena.FrameArena;
|
||||||
|
|
@ -26,6 +27,12 @@ pub const Timer = benchmark.Timer;
|
||||||
pub const FrameTimer = benchmark.FrameTimer;
|
pub const FrameTimer = benchmark.FrameTimer;
|
||||||
pub const AllocationTracker = benchmark.AllocationTracker;
|
pub const AllocationTracker = benchmark.AllocationTracker;
|
||||||
|
|
||||||
|
// Testing utilities
|
||||||
|
pub const TestRunner = testing_utils.TestRunner;
|
||||||
|
pub const SnapshotTester = testing_utils.SnapshotTester;
|
||||||
|
pub const CompareResult = testing_utils.CompareResult;
|
||||||
|
pub const Assertions = testing_utils.Assertions;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -34,4 +41,5 @@ test {
|
||||||
_ = arena;
|
_ = arena;
|
||||||
_ = pool;
|
_ = pool;
|
||||||
_ = benchmark;
|
_ = benchmark;
|
||||||
|
_ = testing_utils;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@ pub const Shortcut = shortcuts.Shortcut;
|
||||||
pub const focus_group = @import("core/focus_group.zig");
|
pub const focus_group = @import("core/focus_group.zig");
|
||||||
pub const FocusGroup = focus_group.FocusGroup;
|
pub const FocusGroup = focus_group.FocusGroup;
|
||||||
pub const FocusGroupManager = focus_group.FocusGroupManager;
|
pub const FocusGroupManager = focus_group.FocusGroupManager;
|
||||||
|
pub const accessibility = @import("core/accessibility.zig");
|
||||||
|
pub const A11yRole = accessibility.Role;
|
||||||
|
pub const A11yState = accessibility.State;
|
||||||
|
pub const A11yInfo = accessibility.Info;
|
||||||
|
pub const A11yManager = accessibility.Manager;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Macro system
|
// Macro system
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue