#include #include #include #include #include using namespace faset; namespace { void check(bool value, const std::string& message) { if (!value) throw std::runtime_error(message); } render::Event key(std::string name, bool control = false) { render::Event e; e.type = render::Event::Type::KeyDown; e.key = std::move(name); e.control = control; return e; } void click(editor::EditorUI& ui, const std::string& id) { const auto* w = ui.widgets().find(id); check(w, "Missing widget: " + id); const auto r = w->rect.intersection(w->clip); check(r.width > 0 && r.height > 0, "Hidden widget: " + id); render::Event down; down.type = render::Event::Type::MouseDown; down.button = 1; down.x = r.x + r.width * .5f; down.y = r.y + r.height * .5f; auto up = down; up.type = render::Event::Type::MouseUp; ui.frame({down, up}); } void text(editor::EditorUI& ui, const std::string& id, const std::string& value) { click(ui, id); render::Event input; input.type = render::Event::Type::TextInput; input.text = value; ui.frame({key("A", true), input, key("Return")}); } void open(editor::EditorUI& ui) { click(ui, "menu-File"); click(ui, "project-settings"); } } // namespace int main() { const auto root = std::filesystem::temp_directory_path() / ("faset-project-settings-ui-" + new_id()); try { const auto project_file = root / "project.faset.json"; atomic_write_json(project_file, {{"format", "faset.project"}, {"version", 1}, {"id", new_id()}, {"name", "Original project"}, {"dimension", 3}, {"editor", {{"script_editor", {"zed", "{file}"}}}}, {"custom_metadata", {{"preserve", true}}}}); editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root}); render::Renderer renderer({1280, 800, "Project settings acceptance", true, true}); editor::EditorUI ui(session, renderer, path_from_utf8(FASET_TEST_ENGINE) / "assets/fonts/NotoSans.ttf", path_from_utf8(FASET_TEST_ENGINE) / "assets/ui/dark.json"); ui.frame({}); click(ui, "add-cube"); session.authoring().save(ui.current_document(), path_from_utf8("Scenes/Главная.scene.json")); const auto other = session.authoring().create("Other", 2); session.authoring().save(other.at("id"), path_from_utf8("Scenes/Другая.scene.json")); ui.frame({}); const auto document_before = session.authoring().query(ui.current_document()); const auto project_before = read_json(project_file); open(ui); text(ui, "project-settings-name", "Unsaved project name"); click(ui, "project-settings-cancel"); check(read_json(project_file) == project_before, "Cancel discards only form draft without writing project"); open(ui); check(ui.widgets().find("project-settings-name")->text == "Original project", "Opening settings reloads current saved values"); check(ui.widgets().find("project-settings-autosave")->checked, "Missing autosave setting appears enabled in Project settings"); check(ui.widgets().focus("project-settings-autosave"), "Keyboard focus reaches the Autosave toggle"); ui.frame({key("Space")}); check(!ui.widgets().find("project-settings-autosave")->checked, "Space toggles Autosave from the keyboard"); text(ui, "project-settings-name", "Новый проект"); click(ui, "project-settings-2d"); click(ui, "project-scene-choice-1"); check(ui.widgets().find("project-settings-start")->text == "Scenes/Другая.scene.json", "Saved scene chooser sets project-relative path"); click(ui, "project-settings-save"); auto saved = read_json(project_file); check(saved["name"] == "Новый проект" && saved["dimension"] == 2 && saved["start_scene"] == "Scenes/Другая.scene.json" && saved["editor"]["autosave"] == false, "Explicit Save project persists typed settings"); check(saved["custom_metadata"] == project_before["custom_metadata"] && saved["id"] == project_before["id"] && saved["editor"]["script_editor"] == project_before["editor"]["script_editor"], "Project settings preserve unknown metadata and project identity"); ui.frame({}); check(ui.widgets().find("autosave-status")->text == "Autosave off", "Disabling Autosave is visible in the Editor status bar"); open(ui); check(!ui.widgets().find("project-settings-autosave")->checked, "Autosave preference is restored when reopening Project settings"); click(ui, "project-settings-autosave"); click(ui, "project-settings-save"); saved = read_json(project_file); check(saved["editor"]["autosave"] == true, "Autosave can be enabled again through Project settings"); check(session.authoring().query(ui.current_document()) == document_before, "Project settings do not change current document, dimension, revision or Undo"); open(ui); text(ui, "project-settings-start", "Scenes/Missing.scene.json"); click(ui, "project-settings-save"); check(ui.widgets().find("project-settings-panel")->visible && !ui.widgets().find("project-settings-error")->text.empty() && read_json(project_file) == saved, "Invalid start scene keeps saved project and form open"); click(ui, "project-settings-reload"); text(ui, "project-settings-name", "Local draft"); const auto external = session.commands().call("faset_project_settings_get", Json::object()); session.commands().call( "faset_project_settings_set", {{"revision", external.at("revision")}, {"settings", {{"name", "External edit"}}}}); click(ui, "project-settings-save"); check(ui.widgets().find("project-settings-panel")->visible && read_json(project_file)["name"] == "External edit" && ui.widgets().find("project-settings-name")->text == "Local draft", "Revision conflict preserves external project and local draft"); check(ui.widgets().find("project-settings-error")->text.find("reload") != std::string::npos, "Conflict directs user to reload"); click(ui, "project-settings-reload"); check(ui.widgets().find("project-settings-name")->text == "External edit", "Explicit Reload saved replaces stale form draft"); click(ui, "project-settings-3d"); click(ui, "project-scene-choice-0"); click(ui, "project-settings-save"); check(read_json(project_file)["start_scene"] == "Scenes/Главная.scene.json", "Save after explicit reload uses new content revision"); check(session.authoring().query(ui.current_document()) == document_before, "Project settings remain outside scene Undo throughout conflicts"); const auto original_document = ui.current_document(); const auto revision = session.authoring().query(original_document).at("revision").get(); session.authoring().transact( original_document, revision, Json::array({{{"op", "scene.rename"}, {"name", "Pending autosave"}}})); ui.frame({}); check(ui.widgets().find("autosave-status")->text == "Pending autosave", "Status bar shows pending scene save after edit"); session.authoring().save(original_document); ui.frame({}); check(ui.widgets().find("autosave-status")->text == "Saved", "Status bar returns to Saved after the scene reaches disk"); session.authoring().transact( original_document, session.authoring().query(original_document).at("revision"), Json::array({{{"op", "scene.rename"}, {"name", "Conflicting autosave"}}})); ui.frame({}); const auto disk_path = root / path_from_utf8("Scenes/Главная.scene.json"); auto disk_scene = read_json(disk_path); disk_scene["name"] = "External scene edit"; atomic_write_json(disk_path, disk_scene); std::this_thread::sleep_for(std::chrono::milliseconds(2100)); ui.frame({}); check(ui.widgets().find("autosave-status")->text == "Save conflict" && ui.widgets().find("autosave-status")->tooltip.find("Save As") != std::string::npos, "Disk conflict remains visible with Save As guidance"); renderer.render(ui.snapshot()); renderer.capture(root / "autosave-conflict.ppm"); check(ui.widgets().focus("autosave-save-as"), "Keyboard focus reaches the recovery Save As action"); ui.frame({key("Return")}); check(ui.widgets().find("menu-popup")->visible && ui.widgets().find("save-path")->visible, "Recovery action opens an editable Save As path"); ui.frame({key("Escape")}); const auto unnamed = session.authoring().create("Unsaved", 2); session.authoring().transact( unnamed.at("id"), unnamed.at("revision"), Json::array({{{"op", "scene.rename"}, {"name", "Needs a path"}}})); ui.select_document(unnamed.at("id")); ui.frame({}); check(ui.widgets().find("autosave-status")->text == "Save As required" && ui.widgets().find("autosave-save-as")->visible, "Unnamed dirty scene is never assigned an implicit path"); open(ui); renderer.render(ui.snapshot()); renderer.capture(root / "project-settings.ppm"); ui.frame({key("Escape")}); check(!ui.widgets().find("project-settings-panel")->visible, "Escape closes project settings without saving"); check(renderer.stats().validation_errors == 0, "Project settings Vulkan validation"); std::cout << "Project settings name/type/start-scene Save/Cancel/Reload/conflict, metadata " "preservation and separate scene Undo passed. " << path_to_utf8(root) << '\n'; return 0; } catch (const std::exception& e) { std::cerr << e.what() << "\nRetained: " << path_to_utf8(root) << '\n'; return 1; } }