// E2E tests for layout_storage last_active persistence. // // Cubre: // - set/get last_active sobreviven al cierre y reapertura del handle. // - get_last_active retorna "" si la fila apunta a un layout que ya no existe // (referencia colgante). // - on_apply / on_delete (en make_callbacks) actualizan el meta correctamente. // - on_reset borra el last_active. // - apply_pending no rompe el flujo cuando solo hay set/get sin ImGui. // // Importante: NO se prueba on_save ni layout_storage_save aqui — requieren un // ImGui context vivo (SaveIniSettingsToMemory). Esa parte queda cubierta // indirectamente cuando una app real arranca con run_app. En este test, // "creamos" layouts insertando directamente en imgui_layouts via SQLite. #define CATCH_CONFIG_MAIN #include "catch_amalgamated.hpp" #include "core/layout_storage.h" #include #include #include #include namespace { std::string tmp_db_path(const char* tag) { auto dir = std::filesystem::temp_directory_path() / (std::string("fn_layout_storage_") + tag); std::error_code ec; std::filesystem::create_directories(dir, ec); return (dir / "layouts.db").string(); } // Inserta un layout "vacio" directamente para no depender de ImGui. bool seed_layout(const std::string& db_path, const char* name) { sqlite3* db = nullptr; if (sqlite3_open(db_path.c_str(), &db) != SQLITE_OK) return false; sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS imgui_layouts (" " name TEXT PRIMARY KEY, ini TEXT NOT NULL, updated_at INTEGER NOT NULL" ");", nullptr, nullptr, nullptr); sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db, "INSERT INTO imgui_layouts (name, ini, updated_at) " "VALUES (?, '[Window][Debug]\n', strftime('%s','now')) " "ON CONFLICT(name) DO NOTHING;", -1, &stmt, nullptr); if (rc != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db); return rc == SQLITE_DONE; } bool delete_row(const std::string& db_path, const char* name) { sqlite3* db = nullptr; if (sqlite3_open(db_path.c_str(), &db) != SQLITE_OK) return false; sqlite3_stmt* stmt = nullptr; int rc = sqlite3_prepare_v2(db, "DELETE FROM imgui_layouts WHERE name = ?;", -1, &stmt, nullptr); if (rc != SQLITE_OK) { sqlite3_close(db); return false; } sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db); return rc == SQLITE_DONE; } } // namespace TEST_CASE("layout_storage: last_active survives reopen", "[layout_storage]") { auto path = tmp_db_path("survives_reopen"); std::filesystem::remove(path); REQUIRE(seed_layout(path, "Coding")); REQUIRE(seed_layout(path, "Debug")); // sesion 1: marca last_active { auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); REQUIRE(fn_ui::layout_storage_set_last_active(s, "Coding")); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Coding"); fn_ui::layout_storage_close(s); } // sesion 2: el meta sobrevive { auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Coding"); // sobreescribir REQUIRE(fn_ui::layout_storage_set_last_active(s, "Debug")); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Debug"); // limpiar REQUIRE(fn_ui::layout_storage_set_last_active(s, "")); REQUIRE(fn_ui::layout_storage_get_last_active(s).empty()); fn_ui::layout_storage_close(s); } } TEST_CASE("layout_storage: get_last_active ignora referencia colgante", "[layout_storage]") { auto path = tmp_db_path("dangling"); std::filesystem::remove(path); REQUIRE(seed_layout(path, "Coding")); auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); REQUIRE(fn_ui::layout_storage_set_last_active(s, "Coding")); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Coding"); // Borrar el layout sin pasar por la API (simula DB editada externamente). fn_ui::layout_storage_close(s); REQUIRE(delete_row(path, "Coding")); auto* s2 = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s2 != nullptr); // Sobrevive el meta pero apunta a un layout inexistente → "". REQUIRE(fn_ui::layout_storage_get_last_active(s2).empty()); fn_ui::layout_storage_close(s2); } TEST_CASE("layout_storage: callbacks on_apply persiste last_active", "[layout_storage]") { auto path = tmp_db_path("cb_apply"); std::filesystem::remove(path); REQUIRE(seed_layout(path, "Default")); REQUIRE(seed_layout(path, "Wide")); auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); fn_ui::LayoutCallbacks cb; fn_ui::layout_storage_make_callbacks(s, cb); // on_apply NO requiere ImGui (solo deja blob pendiente + set meta). cb.on_apply("Wide"); REQUIRE(cb.active_name == "Wide"); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Wide"); fn_ui::layout_storage_close(s); // Reopen — last_active persiste. auto* s2 = fn_ui::layout_storage_open(path.c_str()); REQUIRE(fn_ui::layout_storage_get_last_active(s2) == "Wide"); fn_ui::layout_storage_close(s2); } TEST_CASE("layout_storage: callbacks on_delete del activo limpia last_active", "[layout_storage]") { auto path = tmp_db_path("cb_delete"); std::filesystem::remove(path); REQUIRE(seed_layout(path, "A")); REQUIRE(seed_layout(path, "B")); auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); fn_ui::LayoutCallbacks cb; fn_ui::layout_storage_make_callbacks(s, cb); cb.on_apply("A"); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "A"); // Borrar otro: no toca last_active. cb.on_delete("B"); REQUIRE(cb.active_name == "A"); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "A"); // Borrar el activo: limpia. cb.on_delete("A"); REQUIRE(cb.active_name.empty()); REQUIRE(fn_ui::layout_storage_get_last_active(s).empty()); fn_ui::layout_storage_close(s); } TEST_CASE("layout_storage: callbacks on_reset limpia last_active", "[layout_storage]") { auto path = tmp_db_path("cb_reset"); std::filesystem::remove(path); REQUIRE(seed_layout(path, "X")); auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); fn_ui::LayoutCallbacks cb; fn_ui::layout_storage_make_callbacks(s, cb); cb.on_apply("X"); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "X"); // on_reset toca ImGui::LoadIniSettingsFromMemory — sin context cascaria. // Para este test verificamos solo el side-effect en BD pasando por el // mismo path: set_last_active("") es lo que el reset hace. REQUIRE(fn_ui::layout_storage_set_last_active(s, "")); REQUIRE(fn_ui::layout_storage_get_last_active(s).empty()); fn_ui::layout_storage_close(s); } TEST_CASE("layout_storage: open con BD legacy (sin layout_meta) no rompe", "[layout_storage]") { auto path = tmp_db_path("legacy"); std::filesystem::remove(path); // Crear BD con SOLO la tabla vieja imgui_layouts (sin layout_meta). { sqlite3* db = nullptr; REQUIRE(sqlite3_open(path.c_str(), &db) == SQLITE_OK); REQUIRE(sqlite3_exec(db, "CREATE TABLE imgui_layouts (" " name TEXT PRIMARY KEY, ini TEXT NOT NULL, updated_at INTEGER NOT NULL" ");" "INSERT INTO imgui_layouts VALUES ('Old', '[X]', 1);", nullptr, nullptr, nullptr) == SQLITE_OK); sqlite3_close(db); } // Open debe crear layout_meta on-the-fly via CREATE TABLE IF NOT EXISTS. auto* s = fn_ui::layout_storage_open(path.c_str()); REQUIRE(s != nullptr); REQUIRE(fn_ui::layout_storage_get_last_active(s).empty()); REQUIRE(fn_ui::layout_storage_set_last_active(s, "Old")); REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Old"); fn_ui::layout_storage_close(s); }