Skip to content

Commit dd178b8

Browse files
chrisdpurcellclaude
andcommitted
feat: implement Save As (was stubbed)
Replaces the "coming soon" lambda with _on_action_save_as(), which opens QFileDialog.getSaveFileName, sets the filename field, and delegates to _on_save_clicked(). Adds 2 integration tests; also wires mock_file_svc.save_file to the real FileService so file-content assertions work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5f9fb1a commit dd178b8

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

src/views/main_window.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,7 @@ def _connect_signals(self) -> None:
221221
self._action_quit.triggered.connect(QApplication.quit)
222222
self._action_save.triggered.connect(self._on_save_clicked)
223223
self._action_open.triggered.connect(self._on_action_open)
224-
self._action_save_as.triggered.connect(
225-
lambda: self.ui.statusBar().showMessage("Save As — coming soon")
226-
)
224+
self._action_save_as.triggered.connect(self._on_action_save_as)
227225
self._action_about.triggered.connect(self._on_action_about)
228226
self._action_preferences.triggered.connect(
229227
lambda: self.ui.statusBar().showMessage("Preferences — coming soon")
@@ -359,6 +357,20 @@ def _on_action_open(self) -> None:
359357
self._file_name_edit.setText(path)
360358
self._viewmodel.load_file(path)
361359

360+
def _on_action_save_as(self) -> None:
361+
"""Open a Save As dialog; save to the chosen path if confirmed."""
362+
_glob = " ".join(TEXT_FILE_EXTENSIONS)
363+
initial = self._file_name_edit.text() or QDir.homePath()
364+
path, _ = QFileDialog.getSaveFileName(
365+
self.ui,
366+
"Save As",
367+
initial,
368+
f"Text Files ({_glob});;All Files (*)",
369+
)
370+
if path:
371+
self._file_name_edit.setText(path)
372+
self._on_save_clicked()
373+
362374
def _on_action_about(self) -> None:
363375
"""Show an About dialog."""
364376
QMessageBox.about(

tests/integration/test_main_window.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@
1414

1515
@pytest.fixture
1616
def mock_file_svc():
17+
from src.services.file_service import FileService
18+
1719
svc = MagicMock()
1820
svc.open_file.return_value = TextDocument(
1921
filepath="/tmp/test.txt", content="hello world", encoding="utf-8"
2022
)
23+
# Delegate save_file to the real FileService so tests that write files to
24+
# tmp_path can verify disk contents. Tests that only check signals are
25+
# unaffected because they don't inspect file system state.
26+
_real_svc = FileService()
27+
svc.save_file.side_effect = _real_svc.save_file
2128
return svc
2229

2330

@@ -453,3 +460,29 @@ def test_char_count_updates_on_delete_without_cursor_move(self, window, qtbot):
453460
cursor.deleteChar()
454461
qtbot.wait(10)
455462
assert "8 chars" in window._cursor_label.text()
463+
464+
465+
class TestActionSaveAsHandler:
466+
def test_chosen_path_is_saved(self, window, tmp_path, monkeypatch, qtbot):
467+
"""getSaveFileName returns path → file saved to that path."""
468+
out = tmp_path / "renamed.txt"
469+
monkeypatch.setattr(
470+
"src.views.main_window.QFileDialog.getSaveFileName",
471+
lambda *a, **kw: (str(out), ""),
472+
)
473+
window._plain_text_edit.setPlainText("save-as content")
474+
with qtbot.waitSignal(window._viewmodel.file_saved, timeout=2000):
475+
window._on_action_save_as()
476+
assert out.read_text(encoding="utf-8") == "save-as content"
477+
assert window._file_name_edit.text() == str(out)
478+
479+
def test_cancelled_dialog_is_no_op(self, window, monkeypatch):
480+
"""getSaveFileName returns '' → nothing saved."""
481+
monkeypatch.setattr(
482+
"src.views.main_window.QFileDialog.getSaveFileName",
483+
lambda *a, **kw: ("", ""),
484+
)
485+
emitted: list = []
486+
window._viewmodel.file_saved.connect(emitted.append)
487+
window._on_action_save_as()
488+
assert emitted == []

0 commit comments

Comments
 (0)