import os import tempfile import unittest from pathlib import Path from tkinter import ttk from unittest.mock import patch from copienator_gui.app import CopienatorApp from copienator_gui.workflow import command_display from copienator.commands.page_splitter import _selected_inputs from copienator import EvaluationWorkspace @unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display") class GuiConvenienceTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.repository = Path(self.temp.name) self.evaluation = self.repository / "Évaluation avec espaces" self.evaluation.mkdir() self.app = CopienatorApp(self.repository, False, self.evaluation) self.app.update() def tearDown(self): for callback in self.app.tk.splitlist(self.app.tk.call("after", "info")): self.app.after_cancel(callback) self.app.destroy() self.temp.cleanup() @staticmethod def _label_texts(widget): return [ text for child in widget.winfo_children() for text in ( [str(child.cget("text"))] if isinstance(child, ttk.Label) else GuiConvenienceTests._label_texts(child) ) ] def test_reload_detects_added_and_removed_files_without_advancing(self): for name in ("enonce.pdf", "enonce.tex", "correction.tex"): (self.evaluation / name).touch() self.app._reload_inputs() self.assertIn("names", self.app.info_var.get()) (self.repository / "names").touch() self.app._reload_inputs() self.app.update() self.assertEqual(self.app.state_store.step("inputs")["status"], "success") self.assertEqual(self.app.current_step.id, "inputs") (self.evaluation / "enonce.pdf").unlink() self.app._reload_inputs() self.assertEqual(self.app.state_store.step("inputs")["status"], "ready") self.assertIn("enonce.pdf", self.app.description_label.cget("text")) def test_compact_evaluation_input_and_open_folder_button(self): self.assertEqual(int(self.app.evaluation_entry.cget("width")), 42) self.assertEqual( int(self.app.open_evaluation_button.grid_info()["column"]), int(self.app.evaluation_entry.grid_info()["column"]) + 1, ) with patch("copienator_gui.app.open_path") as opened: self.app.open_evaluation_button.invoke() opened.assert_called_once_with(self.evaluation) def test_optional_blank_crop_can_target_a_copy_or_be_skipped(self): copies = self.evaluation/"Copies" copies.mkdir() source = copies/"Copie01.pdf" source.touch() self.app.state_store.update_step("crop_blank_margins", visited=True) self.app.tree.selection_set("crop_blank_margins") self.app.update() self.assertEqual(self.app.current_step.id, "crop_blank_margins") self.assertEqual(str(self.app.skip_button.cget("state")), "normal") self.app.copy_var.set(source.name) self.app._target_selected_copy() command = self.app._make_command() self.assertEqual(command[-4:], ["crop-margins", str(source), "--workers", "5"]) self.app._skip_step() self.assertEqual(self.app.state_store.step("crop_blank_margins")["status"], "skipped") def test_optional_answer_bottom_crop_uses_parallel_workers_and_can_be_skipped(self): answers = self.evaluation / "Copies" / "Copie01" answers.mkdir(parents=True) (answers / "Ex 1.pdf").touch() self.app.state_store.update_step("crop_exercise_bottoms", visited=True) self.app.tree.selection_set("crop_exercise_bottoms") self.app.update() self.assertEqual(self.app.current_step.id, "crop_exercise_bottoms") self.assertEqual(str(self.app.skip_button.cget("state")), "normal") command = self.app._make_command() self.assertEqual( command[-4:], ["crop-answer-bottoms", self.app._evaluation_arg(), "--workers", "5"], ) self.app._skip_step() self.assertEqual( self.app.state_store.step("crop_exercise_bottoms")["status"], "skipped" ) def test_redo_targets_selected_copy_and_copies_runnable_command(self): for folder in ("Copies", "Copies Originales"): (self.evaluation / folder).mkdir() for name in ("Copie01.pdf", "Copie02.pdf"): (self.evaluation / folder / name).touch() self.app.tree.selection_set("page_splitter") self.app.update() self.app.copy_var.set("Copie02.pdf") self.app._target_selected_copy() command = self.app._make_command() target = Path(command[-1]) self.assertEqual(target, self.evaluation / "Copies" / "Copie02.pdf") self.assertEqual(_selected_inputs(EvaluationWorkspace(self.evaluation), target), [self.evaluation / "Copies Originales" / "Copie02.pdf"]) self.app._copy_command() self.assertEqual(self.app.clipboard_get(), command_display(command)) self.app._target_all_pages() self.assertEqual(self.app.arg_vars["target"].get(), self.app._evaluation_arg()) def test_label_detection_can_target_one_copy(self): copies = self.evaluation / "Copies" copies.mkdir() for name in ("Copie01.pdf", "Copie02.pdf"): (copies / name).touch() self.app.tree.selection_set("labels") self.app.update() self.assertEqual(self.app.copy_var.get(), "Copie01.pdf") self.app.copy_var.set("Copie02.pdf") self.app._target_selected_copy() command = self.app._make_command() self.assertEqual(Path(command[-1]), copies / "Copie02.pdf") def button_texts(widget): return [ text for child in widget.winfo_children() for text in ( [child.cget("text")] if isinstance(child, ttk.Button) else button_texts(child) ) ] controls = button_texts(self.app.form) self.assertIn("Cibler la copie sélectionnée", controls) self.assertIn("Cibler tout le dossier", controls) def test_free_form_arguments_are_shown_only_when_documented(self): self.app.tree.selection_set("labels") self.app.update() labels_text = self._label_texts(self.app.form) extra_label = next( child for child in self.app.form.winfo_children() if isinstance(child, ttk.Label) and str(child.cget("text")).startswith("Arguments supplémentaires") ) tooltip = extra_label._copienator_tooltip self.assertIn("Cutleft", tooltip.text) self.assertFalse(any("Cutleft" in text for text in labels_text)) tooltip.show() self.app.update() self.assertIsNotNone(tooltip.window) self.assertIn("Cutleft", tooltip.window.winfo_children()[0].cget("text")) tooltip.hide() self.app.tree.selection_set("plotting") self.app.update() self.assertFalse( any( text.startswith("Arguments supplémentaires") for text in self._label_texts(self.app.form) ) ) def test_each_visible_argument_label_has_a_tooltip(self): self.app.tree.selection_set("correction") self.app.update() argument_labels = [ child for child in self.app.form.winfo_children() if isinstance(child, ttk.Label) and str(child.cget("text")).endswith("ⓘ") ] self.assertGreaterEqual(len(argument_labels), 4) for label in argument_labels: with self.subTest(label=label.cget("text")): tooltip = getattr(label, "_copienator_tooltip", None) self.assertIsNotNone(tooltip) self.assertTrue(tooltip.text.strip()) def test_verbose_checkbox_updates_supported_commands(self): self.app.tree.selection_set("labels") self.app.update() self.assertNotIn("--verbose", self.app._make_command()) self.app.verbose_var.set(True) self.assertIn("--verbose", self.app._make_command()) def test_console_selection_survives_output_and_is_read_only(self): self.app._append_console("Première ligne\nDeuxième ligne\n") self.app.console.tag_add("sel", "1.0", "1.end") self.app._append_console("Suite\n") self.app._copy_console_selection() self.assertEqual(self.app.clipboard_get(), "Première ligne") self.app._copy_console_all() expected = "Première ligne\nDeuxième ligne\nSuite\n" self.assertEqual(self.app.clipboard_get(), expected) self.app.console.insert("end", "unwanted edit") self.assertEqual(self.app.console.get("1.0", "end-1c"), expected) self.app._select_console_all() self.app._copy_console_selection() self.assertEqual(self.app.clipboard_get(), expected) def test_validate_rename_preserves_files_and_downstream_status(self): pdf = self.evaluation / "Copie01.pdf" pdf.write_bytes(b"unchanged PDF") self.app.state_store.update_step("rename", status="stale") self.app.state_store.update_step("page_splitter", status="success") self.app.tree.selection_set("rename") self.app.update() with patch.object(self.app.runner, "start") as start: self.app.validate_rename_button.invoke() start.assert_not_called() self.assertEqual(self.app.state_store.step("rename")["status"], "success") self.assertEqual(self.app.state_store.step("page_splitter")["status"], "success") self.assertEqual(pdf.read_bytes(), b"unchanged PDF") self.assertEqual(list(self.evaluation.glob("*.pdf")), [pdf]) self.app.state_store.update_step("rename", status="stale") self.app.active_step_id = "page_splitter" self.app._update_controls() self.assertIn("disabled", self.app.validate_rename_button.state()) self.app._validate_rename_step() self.assertEqual(self.app.state_store.step("rename")["status"], "stale") self.app.active_step_id = None