From 44af8fe2d08c69532e24feb50c8bb188ade0a52d Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Sun, 11 Jan 2026 16:37:43 +0800 Subject: [PATCH] Add field tracking tests for text input components This adds new tests to prevent regression of focus/registration bugs in text input components: Registration tests: - test_search_bar_registers: verify search bar registers buffer - test_textfield_with_selection_registers: verify selection variant - test_search_view_registers: verify search modal registers query Focus persistence tests: - test_search_bar_focus_persists: focus maintained across frames - test_unregistered_field_loses_focus: negative test for tracking - test_focus_switch_between_fields: multi-field focus transfer via click State transition tests: - test_disabled_field_clears_focus: disabled clears focus - test_read_only_field_clears_focus: read_only clears focus - test_slider_unrendered_clears_active: slider state cleanup - test_rerender_after_skip_frame: conditional rendering behavior Stress test: - test_many_textfields_stress: 32 fields to test hash collisions These tests would have caught the bug fixed in commit 41c4b76 where text input components failed to register with field tracking system. All focus tests use real mouse input simulation to test actual focus handling logic, not direct state manipulation. --- tests/test-tracking.c | 437 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) diff --git a/tests/test-tracking.c b/tests/test-tracking.c index bb80150..24f3fe7 100644 --- a/tests/test-tracking.c +++ b/tests/test-tracking.c @@ -329,6 +329,432 @@ static void test_reset_field_ids_api(void) PASS(); } +/* Test search_bar registers its buffer for tracking */ +static void test_search_bar_registers(void) +{ + TEST(search_bar_registers); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char search_buf[64] = ""; + size_t cursor = 0; + + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 0, 0, 400, 300, 0); + iui_search_bar(ctx, search_buf, sizeof(search_buf), &cursor, "Search..."); + iui_end_window(ctx); + + /* search_bar should register its buffer */ + ASSERT_TRUE(iui_textfield_is_registered(ctx, search_buf)); + + iui_end_frame(ctx); + + free(buffer); + PASS(); +} + +/* Test textfield_with_selection registers its buffer */ +static void test_textfield_with_selection_registers(void) +{ + TEST(textfield_with_selection_registers); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char text_buf[64] = "Test text"; + iui_edit_state state = {0}; + state.cursor = 4; + + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 0, 0, 400, 300, 0); + iui_textfield_with_selection(ctx, text_buf, sizeof(text_buf), &state, NULL); + iui_end_window(ctx); + + /* textfield_with_selection should register its buffer */ + ASSERT_TRUE(iui_textfield_is_registered(ctx, text_buf)); + + iui_end_frame(ctx); + + free(buffer); + PASS(); +} + +/* Test focus persists across frames when field is rendered each frame. + * This test catches bugs where text input widgets fail to register, + * causing focus to be cleared at frame end. + */ +static void test_search_bar_focus_persists(void) +{ + TEST(search_bar_focus_persists); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char search_buf[64] = ""; + size_t cursor = 0; + + /* Frame 1: Click on search bar to focus it */ + iui_update_mouse_pos(ctx, 200.0f, 150.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_search_bar(ctx, search_buf, sizeof(search_buf), &cursor, "Search..."); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + /* Verify search bar gained focus */ + ASSERT_EQ(ctx->focused_edit, search_buf); + + /* Frame 2: Render search bar again without clicking */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_search_bar(ctx, search_buf, sizeof(search_buf), &cursor, "Search..."); + iui_end_window(ctx); + + /* Focus should persist because search bar registered this frame */ + ASSERT_EQ(ctx->focused_edit, search_buf); + iui_end_frame(ctx); + + /* Focus should still be maintained after frame end */ + ASSERT_EQ(ctx->focused_edit, search_buf); + + free(buffer); + PASS(); +} + +/* Test that unregistered text field loses focus (negative test). + * Verifies the tracking system correctly clears stale focus. + */ +static void test_unregistered_field_loses_focus(void) +{ + TEST(unregistered_field_loses_focus); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char text_buf[32] = "Test"; + + /* Manually set focus without proper registration */ + ctx->focused_edit = text_buf; + + /* Run a frame without rendering the textfield */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 0, 0, 400, 300, 0); + /* Intentionally do NOT render any textfield */ + iui_button(ctx, "Button", IUI_ALIGN_CENTER); + iui_end_window(ctx); + iui_end_frame(ctx); + + /* Focus should be cleared because buffer wasn't registered */ + ASSERT_NULL(ctx->focused_edit); + + free(buffer); + PASS(); +} + +/* Test search_view registers its internal buffer for tracking */ +static void test_search_view_registers(void) +{ + TEST(search_view_registers); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + iui_search_view_state search = {0}; + iui_search_view_open(&search); + + iui_begin_frame(ctx, 1.0f / 60.0f); + + /* Render search view (simulating full screen) */ + bool open = iui_search_view_begin(ctx, &search, 800, 600, "Search..."); + + /* Verify the query buffer inside the state struct is registered */ + ASSERT_TRUE(iui_textfield_is_registered(ctx, search.query)); + + /* Verify it auto-focused */ + ASSERT_EQ(ctx->focused_edit, search.query); + + if (open) + iui_search_view_end(ctx, &search); + iui_end_frame(ctx); + + free(buffer); + PASS(); +} + +/* Test focus switching between multiple text fields. + * Verifies that when focus transfers from one field to another via click, + * both fields remain properly registered with the tracking system. + * Uses real mouse input to test actual focus handling logic. + */ +static void test_focus_switch_between_fields(void) +{ + TEST(focus_switch_between_fields); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char buf1[32] = "Field 1"; + char buf2[32] = "Field 2"; + size_t c1 = 0, c2 = 0; + + /* Frame 1: Click on field 1 to focus it. + * Window at (0,0), field 1 starts after title bar (~40dp). + * Click at y=60 should hit field 1 (height=56dp, so field 1 is ~40-96). + */ + iui_update_mouse_pos(ctx, 150.0f, 60.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 0, 0, 300, 300, 0); + iui_textfield(ctx, buf1, sizeof(buf1), &c1, NULL); + iui_textfield(ctx, buf2, sizeof(buf2), &c2, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + /* Verify Field 1 has focus and both are registered */ + ASSERT_EQ(ctx->focused_edit, buf1); + ASSERT_TRUE(iui_textfield_is_registered(ctx, buf1)); + ASSERT_TRUE(iui_textfield_is_registered(ctx, buf2)); + + /* Frame 2: Click on field 2 to transfer focus. + * Field 2 starts at ~96dp (after field 1), click at y=120. + */ + iui_update_mouse_pos(ctx, 150.0f, 120.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 0, 0, 300, 300, 0); + iui_textfield(ctx, buf1, sizeof(buf1), &c1, NULL); + iui_textfield(ctx, buf2, sizeof(buf2), &c2, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + /* Focus should have transferred to buf2 via actual click handling */ + ASSERT_EQ(ctx->focused_edit, buf2); + + /* Both should still be registered as valid fields */ + ASSERT_TRUE(iui_textfield_is_registered(ctx, buf1)); + ASSERT_TRUE(iui_textfield_is_registered(ctx, buf2)); + + free(buffer); + PASS(); +} + +/* Test that disabled field clears focus when rendered as disabled */ +static void test_disabled_field_clears_focus(void) +{ + TEST(disabled_field_clears_focus); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char buf[32] = "Text"; + size_t cursor = 0; + + /* Frame 1: Click to focus the field */ + iui_update_mouse_pos(ctx, 200.0f, 150.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_textfield(ctx, buf, sizeof(buf), &cursor, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + ASSERT_EQ(ctx->focused_edit, buf); + + /* Frame 2: Same field, but now disabled */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_textfield_options opts = {.disabled = true}; + iui_textfield(ctx, buf, sizeof(buf), &cursor, &opts); + iui_end_window(ctx); + iui_end_frame(ctx); + + /* Focus should be NULL because the field became disabled */ + ASSERT_NULL(ctx->focused_edit); + + /* But it should still be registered (tracking works, logic is in component) + */ + ASSERT_TRUE(iui_textfield_is_registered(ctx, buf)); + + free(buffer); + PASS(); +} + +/* Test that read-only field clears focus when rendered as read_only */ +static void test_read_only_field_clears_focus(void) +{ + TEST(read_only_field_clears_focus); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char buf[32] = "Text"; + size_t cursor = 0; + + /* Frame 1: Click to focus the field */ + iui_update_mouse_pos(ctx, 200.0f, 150.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_textfield(ctx, buf, sizeof(buf), &cursor, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + ASSERT_EQ(ctx->focused_edit, buf); + + /* Frame 2: Same field, but now read_only */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_textfield_options opts = {.read_only = true}; + iui_textfield(ctx, buf, sizeof(buf), &cursor, &opts); + iui_end_window(ctx); + iui_end_frame(ctx); + + /* Focus should be NULL because the field became read_only */ + ASSERT_NULL(ctx->focused_edit); + + free(buffer); + PASS(); +} + +/* Test slider active state is cleared when slider not rendered */ +static void test_slider_unrendered_clears_active(void) +{ + TEST(slider_unrendered_clears_active); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + float value = 50.0f; + + /* Frame 1: Render slider and start interaction */ + iui_update_mouse_pos(ctx, 200.0f, 150.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + value = iui_slider_ex(ctx, value, 0.0f, 100.0f, 1.0f, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + + /* Frame 2: Keep mouse down, slider should be active */ + iui_update_mouse_pos(ctx, 220.0f, 150.0f); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + value = iui_slider_ex(ctx, value, 0.0f, 100.0f, 1.0f, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + + /* Frame 3: Do NOT render the slider */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + /* slider intentionally not rendered */ + iui_button(ctx, "Other", IUI_ALIGN_CENTER); + iui_end_window(ctx); + iui_end_frame(ctx); + + /* Active slider should be cleared */ + ASSERT_EQ(ctx->slider.active_id & IUI_SLIDER_ID_MASK, 0); + + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + free(buffer); + PASS(); +} + +/* Test field skipped for one frame then re-rendered retains ability to focus */ +static void test_rerender_after_skip_frame(void) +{ + TEST(rerender_after_skip_frame); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + + char buf[32] = "Test"; + size_t cursor = 0; + + /* Frame 1: Render and focus textfield */ + iui_update_mouse_pos(ctx, 200.0f, 150.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_textfield(ctx, buf, sizeof(buf), &cursor, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + ASSERT_EQ(ctx->focused_edit, buf); + + /* Frame 2: Skip rendering textfield - focus should be cleared */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_button(ctx, "Other", IUI_ALIGN_CENTER); + iui_end_window(ctx); + iui_end_frame(ctx); + + ASSERT_NULL(ctx->focused_edit); + + /* Frame 3: Re-render textfield and click to refocus */ + iui_update_mouse_pos(ctx, 200.0f, 150.0f); + iui_update_mouse_buttons(ctx, IUI_MOUSE_LEFT, 0); + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 100, 100, 300, 200, 0); + iui_textfield(ctx, buf, sizeof(buf), &cursor, NULL); + iui_end_window(ctx); + iui_end_frame(ctx); + iui_update_mouse_buttons(ctx, 0, IUI_MOUSE_LEFT); + + /* Should be able to regain focus after being re-rendered */ + ASSERT_EQ(ctx->focused_edit, buf); + + free(buffer); + PASS(); +} + +/* Stress test: many text fields to verify hash table handles collisions */ +static void test_many_textfields_stress(void) +{ + TEST(many_textfields_stress); + void *buffer = malloc(iui_min_memory_size()); + iui_context *ctx = create_test_context(buffer, false); + ASSERT_NOT_NULL(ctx); + +#define NUM_FIELDS 32 + char bufs[NUM_FIELDS][16]; + size_t cursors[NUM_FIELDS] = {0}; + + /* Initialize buffers with unique content */ + for (int i = 0; i < NUM_FIELDS; i++) { + snprintf(bufs[i], sizeof(bufs[i]), "Field %d", i); + } + + /* Frame: Render all fields */ + iui_begin_frame(ctx, 1.0f / 60.0f); + iui_begin_window(ctx, "Test", 0, 0, 800, 600, 0); + for (int i = 0; i < NUM_FIELDS; i++) { + iui_textfield(ctx, bufs[i], sizeof(bufs[i]), &cursors[i], NULL); + } + iui_end_window(ctx); + + /* All fields should be registered */ + ASSERT_EQ(ctx->field_tracking.textfield_count, NUM_FIELDS); + + /* Verify each buffer is registered (tests hash collision handling) */ + for (int i = 0; i < NUM_FIELDS; i++) { + ASSERT_TRUE(iui_textfield_is_registered(ctx, bufs[i])); + } + + iui_end_frame(ctx); + +#undef NUM_FIELDS + free(buffer); + PASS(); +} + /* Test Suite Runner */ void run_field_tracking_tests(void) @@ -345,5 +771,16 @@ void run_field_tracking_tests(void) test_duplicate_registration(); test_edit_with_selection_registers(); test_reset_field_ids_api(); + test_search_bar_registers(); + test_textfield_with_selection_registers(); + test_search_bar_focus_persists(); + test_unregistered_field_loses_focus(); + test_search_view_registers(); + test_focus_switch_between_fields(); + test_disabled_field_clears_focus(); + test_read_only_field_clears_focus(); + test_slider_unrendered_clears_active(); + test_rerender_after_skip_frame(); + test_many_textfields_stress(); SECTION_END(); }