The screen could already replay from any step -- Enter on a row does it -- but that lived in a hint line under four named buttons. A capability you have to guess is not offered, and it was reported as missing. It now has a key, a button and a message, like the four others. The b key added earlier went to the line-by-line prompts during the run, not to this screen. Same letter on both, so going back is one key wherever you are. Two defects behind it. Replaying the chosen step actually SKIPPED it, resuming at the next one. And a replayed step did not rewind the variables it owns, so the second run started from the first one's leftovers. Checked in a simulated terminal: b focuses the table, two arrows and Enter return step 2, and the button focuses without choosing. --- FR --- L'écran savait déjà rejouer depuis n'importe quelle étape — Entrée sur une rangée le fait — mais cela vivait dans une ligne d'indication sous quatre boutons nommés. Une capacité qu'il faut deviner n'est pas offerte, et elle a été signalée manquante. Elle a maintenant une touche, un bouton et un message, comme les quatre autres. La touche b ajoutée plus tôt s'adressait aux invites ligne à ligne pendant l'exécution, pas à cet écran. Même lettre sur les deux : revenir en arrière est une seule touche, où qu'on soit. Deux défauts derrière cela. Rejouer l'étape choisie la SAUTAIT en fait, reprenant à la suivante. Et une étape rejouée ne remettait pas à zéro les variables qui lui appartiennent, si bien que la seconde exécution partait des restes de la première. Vérifié en terminal simulé : b donne le focus au tableau, deux flèches et Entrée ramènent l'étape 2, et le bouton met le focus sans choisir. Assisted-by: Claude Opus 5
119 lines
4.2 KiB
Python
119 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
"""L'écran de reprise doit OFFRIR le retour à une étape, pas le cacher.
|
|
|
|
Le mécanisme existait — Entrée sur une ligne du tableau — mais vivait dans une
|
|
ligne d'astuce sous quatre boutons nommés. Une capacité qu'il faut deviner
|
|
n'est pas offerte : elle a donc une touche, un bouton et un message, comme les
|
|
quatre autres actions.
|
|
"""
|
|
|
|
import json
|
|
import unittest
|
|
|
|
from script.todo import todo_i18n
|
|
from script.todo.migration_form import run_resume_tui
|
|
from script.todo.todo_upgrade import TodoUpgrade
|
|
|
|
|
|
def context():
|
|
"""Le contexte d'une migration arrêtée avant les montées de version."""
|
|
progression = {
|
|
"migration_file": "./image_db/technolibre.zip",
|
|
"config_database_name": "technolibre_migration_01_neutralize",
|
|
"target_odoo_version": "18.0",
|
|
"date_create": "2026-08-12 05:04:22",
|
|
"state_0_install_odoo": True,
|
|
"state_1_restore_database": True,
|
|
"state_2_update_all": True,
|
|
"state_3_clean_database": True,
|
|
}
|
|
return TodoUpgrade.resume_context(TodoUpgrade, progression)
|
|
|
|
|
|
class TestBackIsOffered(unittest.TestCase):
|
|
def setUp(self):
|
|
self.addCleanup(
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
)
|
|
todo_i18n._current_lang = "en"
|
|
self.ctx = context()
|
|
|
|
def bindings(self, app):
|
|
out = {}
|
|
for item in app.BINDINGS:
|
|
if isinstance(item, tuple):
|
|
out[item[0]] = item[1]
|
|
else:
|
|
out[item.key] = item.action
|
|
return out
|
|
|
|
def test_b_is_a_binding_like_the_others(self):
|
|
app = run_resume_tui(self.ctx, run_app=False)
|
|
self.assertEqual(self.bindings(app).get("b"), "back_step")
|
|
|
|
def test_the_action_exists(self):
|
|
self.assertTrue(
|
|
hasattr(
|
|
run_resume_tui(self.ctx, run_app=False), "action_back_step"
|
|
)
|
|
)
|
|
|
|
|
|
class TestBackWorks(unittest.IsolatedAsyncioTestCase):
|
|
"""Piloté dans un terminal simulé : la touche mène bien au choix."""
|
|
|
|
def setUp(self):
|
|
self.addCleanup(
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
)
|
|
todo_i18n._current_lang = "en"
|
|
self.ctx = context()
|
|
|
|
async def test_b_focuses_the_steps_and_enter_replays_from_there(self):
|
|
from textual.widgets import DataTable
|
|
|
|
app = run_resume_tui(self.ctx, run_app=False)
|
|
async with app.run_test() as pilot:
|
|
await pilot.press("b")
|
|
await pilot.pause()
|
|
table = app.query_one("#steps", DataTable)
|
|
self.assertTrue(table.has_focus)
|
|
# Le curseur part sur la première étape inachevée ; on remonte de
|
|
# deux pour viser « mettre à jour tous les modules ».
|
|
await pilot.press("up")
|
|
await pilot.press("up")
|
|
await pilot.pause()
|
|
expected = str(self.ctx["steps"][table.cursor_row]["step"])
|
|
await pilot.press("enter")
|
|
await pilot.pause()
|
|
self.assertEqual(app._result["answer"], expected)
|
|
|
|
async def test_there_is_a_button_too(self):
|
|
# C'est l'absence VISIBLE qui a été signalée : une touche seule ne se
|
|
# voit pas parmi quatre boutons. Les widgets ne se lisent qu'une fois
|
|
# l'écran monté, d'où le pilote.
|
|
from textual.widgets import Button
|
|
|
|
app = run_resume_tui(self.ctx, run_app=False)
|
|
async with app.run_test():
|
|
labels = [b.label.plain for b in app.query(Button)]
|
|
self.assertIn("Go back to a step", labels)
|
|
|
|
async def test_the_button_does_the_same(self):
|
|
from textual.widgets import Button, DataTable
|
|
|
|
app = run_resume_tui(self.ctx, run_app=False)
|
|
async with app.run_test() as pilot:
|
|
button = next(b for b in app.query(Button) if b.id == "a_back")
|
|
await pilot.click(button)
|
|
await pilot.pause()
|
|
self.assertTrue(app.query_one("#steps", DataTable).has_focus)
|
|
# Et surtout : le bouton ne répond PAS à la place de l'utilisateur.
|
|
self.assertIsNone(app._result["answer"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|