import json from unittest.mock import patch import dash from selenium.webdriver.support.ui import WebDriverWait import src.app # noqa: F401 # instancie l'app → register_page() from src.auth import db as auth_db from src.pages import tableau from src.saved_views import db as saved_views_db from src.saved_views import resolve from src.utils.query_ast import And, Condition, ast_to_dict def _make_user(email="u@ex.fr"): auth_db.init_schema() return auth_db.create_user(email, "hash") def _seed(uid, name="Ma vue"): ast = And([Condition("objet", "contains", "route")]) query = json.dumps( {"ast": ast_to_dict(ast), "columnState": [{"colId": "montant", "hide": True}]} ) return saved_views_db.upsert(uid, "tableau", name, query) def test_resolve_vue_from_url_found(monkeypatch, users_db_path): monkeypatch.setattr(resolve.ui, "DOMAIN_NAME", "test.colibre.fr") saved_views_db.init_schema() uid = _make_user() token = _seed(uid) out = tableau.resolve_vue_from_url(f"?vue=ma-vue_{token}") assert out["found"] is True assert out["token"] == token def test_resolve_vue_from_url_no_param_returns_none(users_db_path): saved_views_db.init_schema() assert tableau.resolve_vue_from_url("") is None assert tableau.resolve_vue_from_url("?autre=1") is None def test_apply_vue_resolution_found_shows_box(users_db_path): resolution = { "found": True, "filter_model": { "objet": {"filterType": "text", "type": "contains", "filter": "route"} }, "column_state": [{"colId": "montant", "hide": True}], "hidden_columns": ["montant"], "token": "abc123", "url": "https://test.colibre.fr/tableau?vue=ma-vue_abc123", "error": None, } fm, cs, hidden, active, feedback = tableau.apply_vue_resolution(resolution) assert fm == resolution["filter_model"] assert cs == resolution["column_state"] assert hidden == ["montant"] assert active == {"token": "abc123", "url": resolution["url"]} assert feedback == "" def test_apply_vue_resolution_not_found_shows_alert(users_db_path): resolution = { "found": False, "filter_model": None, "column_state": None, "hidden_columns": None, "token": None, "url": None, "error": resolve.NOT_FOUND_MESSAGE, } fm, cs, hidden, active, feedback = tableau.apply_vue_resolution(resolution) assert fm is dash.no_update assert cs is dash.no_update assert active is None assert resolve.NOT_FOUND_MESSAGE in str(feedback) def test_apply_vue_resolution_none_is_noop(users_db_path): out = tableau.apply_vue_resolution(None) assert all(v is dash.no_update for v in out) def _fake_user(user_id): u = type("U", (), {})() u.is_authenticated = True u.id = user_id return u class _Ctx: triggered_id = None def test_render_share_box_visible_when_active(): class_name, value = tableau.render_share_box( {"token": "abc123", "url": "https://x/tableau?vue=a_abc123"} ) assert "d-flex" in class_name assert "d-none" not in class_name assert value == "https://x/tableau?vue=a_abc123" def test_render_share_box_hidden_when_none(): class_name, value = tableau.render_share_box(None) assert "d-none" in class_name assert value == "" def test_apply_saved_view_sets_active(monkeypatch, users_db_path): monkeypatch.setattr(tableau.saved_views_ui, "DOMAIN_NAME", "test.colibre.fr") saved_views_db.init_schema() uid = _make_user() token = _seed(uid, "Ma vue") view_id = saved_views_db.list_views(uid, "tableau")[0]["id"] _Ctx.triggered_id = {"type": "saved-view-item", "index": view_id} monkeypatch.setattr(tableau, "ctx", _Ctx) with patch.object(tableau, "current_user", _fake_user(uid)): out = tableau.apply_saved_view( [1], [{"type": "saved-view-item", "index": view_id}] ) # (filter_model, column_state, hidden, active-view) assert out[3] == { "token": token, "url": f"https://test.colibre.fr/tableau?vue=ma-vue_{token}", } def test_save_view_shows_box(monkeypatch, users_db_path): monkeypatch.setattr(tableau.saved_views_ui, "DOMAIN_NAME", "test.colibre.fr") saved_views_db.init_schema() uid = _make_user() monkeypatch.setattr(tableau, "current_user_has_subscription", lambda: True) with patch.object(tableau, "current_user", _fake_user(uid)): out = tableau.save_view(1, "Nouvelle", {}, []) # (is_open, feedback, refresh, active-view) assert out[3]["url"].startswith("https://test.colibre.fr/tableau?vue=nouvelle_") def _type_in_first_filter(dash_duo): """Saisit du texte dans le premier filtre flottant présent dans le DOM (AG Grid virtualise horizontalement : 'objet' peut être hors DOM). Re-localise l'élément à chaque appel : la grille se re-rend juste après l'application de la vue (echo columnVisible) et remplace l'input.""" selector = "#tableau_grid .ag-floating-filter-input input" dash_duo.wait_for_element(selector, timeout=8) el = dash_duo.find_elements(selector)[0] dash_duo.driver.execute_script( "arguments[0].scrollIntoView({block: 'center', inline: 'center'});", el ) el.click() el.send_keys("z") def test_open_shared_view_applies_and_shows_box(dash_duo, users_db_path): """?vue= applique la vue et affiche le bloc de partage ; une action utilisateur (saisie d'un filtre) le masque via l'écouteur d'événements AG Grid (source utilisateur ≠ 'api'), l'écho de l'application étant ignoré.""" saved_views_db.init_schema() uid = _make_user() token = _seed(uid, "Ma vue") from src.app import app dash_duo.start_server(app) dash_duo.wait_for_text_to_equal(".logo > h1", "colibre", timeout=6) dash_duo.wait_for_page(dash_duo.server_url + f"/tableau?vue=ma-vue_{token}") # Le bloc de partage est visible et affiche l'URL courte (jeton). Le texte est # renseigné par le même callback que l'affichage : le lire non vide prouve que # l'écho de l'application n'a PAS masqué le bloc. dash_duo.wait_for_style_to_equal("#share-url-box", "display", "flex", timeout=10) dash_duo.wait_for_element("#share-url-text", timeout=6) WebDriverWait(dash_duo.driver, 10).until( lambda _d: token in (dash_duo.find_element("#share-url-text").text or "") ) # Le bouton de copie porte un libellé explicite (UX : pas d'icône seule). assert "Copier le lien" in dash_duo.find_element("#share-url-box").text # Une action utilisateur (filtre) masque la box. On re-tente la saisie tant # que le bloc n'est pas masqué : la fenêtre de re-render post-application # peut invalider l'input entre le find et le send_keys. def _box_hidden(_d): _type_in_first_filter(dash_duo) display = dash_duo.find_element("#share-url-box").value_of_css_property( "display" ) return display == "none" WebDriverWait(dash_duo.driver, 15).until(_box_hidden)