Add new empty scheme creation action and support empty JSON load

- Добавлено действие Создать... (Ctrl+N) в меню Файл для инициализации нового файла схемы.
- Реализована автоматическая запись дефолтной схемы circles при открытии пустого или содержащего только пробелы json-файла.
- Добавлены соответствующие автоматические тесты в qa_smoke.py.
This commit is contained in:
Данил Ярош 2026-07-31 14:54:27 +03:00
parent fa1497bee7
commit c3b98d7fe8
3 changed files with 110 additions and 1 deletions

View File

@ -336,6 +336,7 @@ class MainWindow(QMainWindow):
self.actions[key] = action
return action
add_action("new", "Создать...", self.new_file, "file-plus", QKeySequence.New)
add_action("open", "Открыть", self.open_file, "folder-open", QKeySequence.Open)
add_action("save", "Сохранить", self.save_file, "save", QKeySequence.Save)
add_action("save_as", "Сохранить как", self.save_file_as, "save-all", QKeySequence.SaveAs)
@ -446,6 +447,7 @@ class MainWindow(QMainWindow):
menu_bar = self.menuBar()
menu_bar.clear()
file_menu = menu_bar.addMenu("Файл")
file_menu.addAction(self.actions["new"])
file_menu.addAction(self.actions["open"])
file_menu.addAction(self.actions["save"])
file_menu.addAction(self.actions["save_as"])
@ -904,6 +906,43 @@ class MainWindow(QMainWindow):
action.setText("Скрыть боковую панель")
action.setToolTip("Скрыть боковую панель")
def new_file(self) -> None:
if not self._confirm_discard_changes():
return
path, _ = QFileDialog.getSaveFileName(
self,
"Создать новую схему",
str(APP_DIR),
"JSON files (*.json);;All files (*.*)",
)
if not path:
return
p = Path(path)
if not p.suffix:
p = p.with_suffix(".json")
try:
p.write_text('{"circles": []}', encoding="utf-8")
except Exception as exc:
show_critical(self, "Ошибка создания файла", str(exc))
return
loaded = MappingDocument()
try:
loaded.load(p)
except Exception as exc:
show_critical(self, "Ошибка загрузки нового файла", str(exc))
return
self.document = loaded
self._dirty = False
self._undo_stack.clear()
self._redo_stack.clear()
self.active_line_id = None
self.canvas.set_document(self.document)
self.canvas.set_active_line(self.active_line_id)
self.refresh_tree()
self._sync_canvas_to_active_line()
self.status.setText(f"Создана новая пустая схема: {p}")
self._refresh_ui_state()
def open_file(self) -> None:
if not self._confirm_discard_changes():
return

View File

@ -43,6 +43,16 @@ _LED_POINT_KNOWN_FIELDS = {
def load_json_document(path: str | Path) -> dict[str, Any]:
path = Path(path)
if path.exists():
try:
if path.stat().st_size == 0:
path.write_text('{"circles": []}', encoding="utf-8")
else:
content = path.read_bytes()
if not content.strip():
path.write_text('{"circles": []}', encoding="utf-8")
except Exception:
pass
text, was_converted = _read_text_as_utf8(path)
if was_converted:
path.write_text(text, encoding="utf-8")

View File

@ -69,6 +69,13 @@ except ImportError:
QDialogButtonBox = None
QGraphicsItem = None
QMessageBox = None
QColor = None
QImage = None
QKeySequence = None
QApplication = None
QDialogButtonBox = None
QGraphicsItem = None
QMessageBox = None
QToolBar = None
@ -77,6 +84,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent
def main() -> None:
tests: list[Callable[[], None]] = [
test_load_empty_json_file,
test_load_supported_json_files,
test_load_raw_json_with_trailing_commas_and_bare_decimal_points,
test_non_utf8_json_is_converted_to_utf8_on_load,
@ -121,6 +129,7 @@ def main() -> None:
test_panel_mapping_generation_with_strategies,
test_block_renaming_and_naming_strategy,
test_line_renaming_and_tree_shortcuts,
test_new_file_action,
test_export_mapping_to_image,
test_save_preserves_unknown_source_fields,
test_ordered_json_preserves_document_and_remaps_uids,
@ -221,6 +230,25 @@ def main() -> None:
print(f"All {len(tests)} QA smoke checks passed.")
def test_load_empty_json_file() -> None:
with tempfile.TemporaryDirectory() as temp_dir:
# Test 1: Completely empty file (0 bytes)
path = Path(temp_dir) / "empty.json"
path.write_bytes(b"")
doc = MappingDocument()
doc.load(path)
assert len(doc.points) == 0
assert path.read_text(encoding="utf-8") == '{"circles": []}'
# Test 2: Whitespace-only file
path_ws = Path(temp_dir) / "whitespace.json"
path_ws.write_text(" \n \t ", encoding="utf-8")
doc_ws = MappingDocument()
doc_ws.load(path_ws)
assert len(doc_ws.points) == 0
assert path_ws.read_text(encoding="utf-8") == '{"circles": []}'
def test_load_supported_json_files() -> None:
fixtures = [
PROJECT_ROOT / "circles.json",
@ -1132,6 +1160,31 @@ def test_line_renaming_and_tree_shortcuts() -> None:
window.close()
def test_new_file_action() -> None:
if MainWindow is None or QApplication is None:
return
app = QApplication.instance() or QApplication([])
window = MainWindow()
from PySide6.QtWidgets import QFileDialog
with tempfile.TemporaryDirectory() as temp_dir:
new_path = Path(temp_dir) / "created_scheme.json"
original_getSaveFileName = QFileDialog.getSaveFileName
QFileDialog.getSaveFileName = lambda *args, **kwargs: (str(new_path), "JSON files (*.json)")
try:
window.new_file()
finally:
QFileDialog.getSaveFileName = original_getSaveFileName
assert new_path.exists()
assert new_path.stat().st_size > 0
assert len(window.document.points) == 0
assert window.document.path == new_path
window.close()
def test_export_mapping_to_image() -> None:
if QApplication is None:
@ -1856,6 +1909,11 @@ def test_canvas_refresh_does_not_accumulate_connection_items() -> None:
assert len(canvas.items_by_id) == len(doc.points)
def test_main_window_has_classic_menu_bar() -> None:
if MainWindow is None or QApplication is None:
return
app = QApplication.instance() or QApplication([])
window = MainWindow()
def test_main_window_has_classic_menu_bar() -> None:
if MainWindow is None or QApplication is None:
return
@ -1877,7 +1935,7 @@ def test_classic_menu_contains_expected_actions() -> None:
app.processEvents()
expected = {
"Файл": ["Открыть", "Сохранить", "Сохранить как", "Сгенерировать скрипт", "Параметры"],
"Файл": ["Создать...", "Открыть", "Сохранить", "Сохранить как", "Сгенерировать скрипт", "Параметры"],
"Правка": ["Отменить", "Повторить", "Удалить фиксацию", "Удалить точки", "Удалить все", "Очистить выбор"],
"Вид": [
"Показать всё",
@ -1974,6 +2032,7 @@ def test_main_window_actions_use_lucide_icons() -> None:
app.processEvents()
expected_icon_actions = {
"new",
"open",
"save",
"save_as",
@ -2017,6 +2076,7 @@ def test_main_window_action_shortcuts_are_assigned() -> None:
app.processEvents()
expected = {
"new": "Ctrl+N",
"generate_script": "Ctrl+G",
"settings": "Ctrl+,",
"delete_points": "Ctrl+Del",