303 lines
13 KiB
Python
303 lines
13 KiB
Python
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.missing_requirements_label.cget("text"))
|
|
style = ttk.Style(self.app)
|
|
self.assertEqual(
|
|
style.lookup("MissingPrerequisite.TLabel", "foreground"), "#c62828"
|
|
)
|
|
|
|
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_manual_resolution_panel_follows_command_target(self):
|
|
manual = self.evaluation / "manual_resolutions.txt"
|
|
manual.write_text("Copie01 A -> B|\n", encoding="utf-8")
|
|
self.app.tree.selection_set("manual_resolution")
|
|
self.app.update()
|
|
self.assertEqual(len(self.app.manual_panel.pdf_buttons), 2)
|
|
other = self.repository / "Other"
|
|
other.mkdir()
|
|
other_manual = other / "manual_resolutions.txt"
|
|
other_manual.write_text("Copie02 C xs D\n", encoding="utf-8")
|
|
self.app.arg_vars["target"].set(str(other))
|
|
self.app.update()
|
|
self.assertIn("Copie02", self.app.manual_panel.text.get("1.0", "end"))
|
|
with patch("copienator_gui.manual_resolution.open_path") as opened:
|
|
self.app.manual_panel.editor_button.invoke()
|
|
opened.assert_called_once_with(other_manual)
|
|
|
|
def test_batch_status_waits_until_all_jobs_are_ready(self):
|
|
self.app.state_store.update_step("correction", variant="batch")
|
|
self.app.state_store.update_step("batch_status", visited=True)
|
|
self.app.tree.selection_set("batch_status")
|
|
self.app.update()
|
|
command = self.app._make_command()
|
|
self.assertIn("--evaluation", command)
|
|
self.assertFalse(self.app.arg_vars)
|
|
self.app.active_step_id = "batch_status"
|
|
self.app._finish_process(4, False)
|
|
self.app.update()
|
|
self.assertEqual(self.app.current_step.id, "batch_status")
|
|
self.app.active_step_id = "batch_status"
|
|
self.app._finish_process(0, False)
|
|
self.app.update()
|
|
self.assertEqual(self.app.current_step.id, "fetch_batches")
|
|
|
|
def test_batch_watch_buttons_use_loaded_evaluation_and_stop_on_reload(self):
|
|
self.app.state_store.update_step("correction", variant="batch")
|
|
self.app.tree.selection_set("batch_status")
|
|
self.app.update()
|
|
with patch.object(self.app.batch_monitor, "start") as start:
|
|
self.app.watch_batches_button.invoke()
|
|
self.assertEqual(start.call_args.args[0][-2:], ["--evaluation", str(self.evaluation)])
|
|
self.app.batch_monitor.active = True
|
|
self.app._update_controls()
|
|
self.assertEqual(str(self.app.stop_watch_batches_button.cget("state")), "normal")
|
|
self.app.stop_watch_batches_button.invoke()
|
|
self.assertFalse(self.app.batch_monitor.active)
|
|
self.app.batch_monitor.active = True
|
|
self.app._load_evaluation()
|
|
self.assertFalse(self.app.batch_monitor.active)
|
|
|
|
def test_background_batch_readiness_notifies_without_changing_other_step(self):
|
|
self.app.tree.selection_set("inputs")
|
|
self.app.update()
|
|
with patch("copienator_gui.app.notify_desktop") as notify:
|
|
self.app._batch_results_ready()
|
|
notify.assert_called_once()
|
|
self.assertIn(self.evaluation.name, notify.call_args.args[1])
|
|
self.assertEqual(self.app.current_step.id, "inputs")
|
|
self.assertEqual(self.app.state_store.step("batch_status")["status"], "success")
|
|
|
|
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
|