"""UI tests for the per-connector tab layout.""" import pytest def _auth(token): return {"Authorization": f"Bearer {token}"} def test_admin_tables_renders_tab_nav(seeded_app): """Page has tab nav with at least the source types configured for the instance plus Jira (always shown when any Jira rows exist).""" c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/tables", headers=_auth(token)) assert r.status_code == 200 html = r.text assert 'role="tablist"' in html or 'class="tab-nav"' in html assert 'data-tab="bigquery"' in html or 'id="tab-bigquery"' in html assert 'data-tab="keboola"' in html or 'id="tab-keboola"' in html def test_admin_tables_active_tab_matches_instance_type(seeded_app, monkeypatch): """When data_source.type='bigquery', the BigQuery tab is the initially-active one. Operator can still switch to Keboola tab if they want to register a secondary source.""" fake_cfg = {"data_source": {"type": "bigquery", "bigquery": {"project": "p"}}} monkeypatch.setattr( "app.instance_config.load_instance_config", lambda: fake_cfg, raising=False, ) from app.instance_config import reset_cache reset_cache() try: c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/tables", headers=_auth(token)) html = r.text # The BQ tab content is the visible one initially. # Either a class="active" on the BQ tab button, or aria-selected="true". assert ( 'data-tab="bigquery" class="tab active"' in html or 'data-tab="bigquery" aria-selected="true"' in html ) finally: reset_cache() def test_admin_tables_each_tab_has_register_button(seeded_app): """Each writable source tab has its own Register button. Jira is read-only (no Register).""" c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/tables", headers=_auth(token)) html = r.text # Each Register button is scoped to its tab — id distinguishes. # We check presence of the registration trigger elements. assert 'id="bqRegisterBtn"' in html or 'data-register-source="bigquery"' in html assert 'id="kbRegisterBtn"' in html or 'data-register-source="keboola"' in html # No Jira register button (Jira is webhook-driven). assert 'data-register-source="jira"' not in html def test_admin_tables_listing_per_tab(seeded_app): """The registry table is rendered per tab — each tab has its own
filtered by source_type. Listing JS reads tables from the catalog API and routes each row into the matching tab's .""" c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/tables", headers=_auth(token)) html = r.text assert 'id="bqTableListing"' in html assert 'id="kbTableListing"' in html assert 'id="jiraTableListing"' in html def test_jira_tab_is_read_only(seeded_app): """Phase G: Jira tables are populated by webhooks, not by admin registration. Tab shows the listing + a hint pointing to docs; no Register button.""" c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/tables", headers=_auth(token)) html = r.text jira_tab = html[html.index('id="tab-content-jira"'):] jira_tab = jira_tab[:jira_tab.index('')] # No Register button. assert 'data-register-source="jira"' not in jira_tab assert 'jiraRegisterBtn' not in jira_tab # Hint pointing to docs (webhook-driven model). assert "webhook" in jira_tab.lower() # Listing div present. assert 'id="jiraTableListing"' in jira_tab def test_admin_tables_tab_persists_in_url_hash(seeded_app): """Tab switching updates window.location.hash so refresh keeps the operator on the right tab. Verify the JS hooks for it are present.""" c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/tables", headers=_auth(token)) html = r.text assert "location.hash" in html or "history.replaceState" in html # And initial-tab pickup from hash on load. assert "window.location.hash" in html or "getActiveTabFromHash" in html def test_listing_partitions_rows_by_source_type(seeded_app): """When the operator has registered tables across all three sources, each tab's listing shows only the rows matching its source_type. JS-driven so we test by inspecting the JS branching logic indirectly: the renderer function takes a source filter and emits rows accordingly.""" c = seeded_app["client"] token = seeded_app["admin_token"] auth = {"Authorization": f"Bearer {token}"} c.post("/api/admin/register-table", headers=auth, json={ "name": "kb_table", "source_type": "keboola", "bucket": "in.c-x", "source_table": "y", "query_mode": "local", }) c.post("/api/admin/register-table", headers=auth, json={ "name": "bq_table", "source_type": "bigquery", "query_mode": "materialized", "source_query": "SELECT 1", }) r = c.get("/admin/tables", headers=auth) html = r.text # The renderer function is dispatched per tab. The test verifies the # JS code paths exist (we don't run JS in tests, just confirm the # template provides the wiring). assert "renderRegistryListing" in html or "loadRegistry" in html # Each tab listing div is the renderer target. assert "document.getElementById('bqTableListing')" in html assert "document.getElementById('kbTableListing')" in html assert "document.getElementById('jiraTableListing')" in html def test_registry_listing_renders_manage_access_button(seeded_app): """Phase O: each row in the per-tab listing has a Manage access button that links to /admin/access scoped to the table_id.""" c = seeded_app["client"] token = seeded_app["admin_token"] auth = {"Authorization": f"Bearer {token}"} # Register a table so the listing has at least one row to render. c.post( "/api/admin/register-table", headers=auth, json={ "name": "test_orders", "source_type": "keboola", "bucket": "in.c-sales", "source_table": "orders", "query_mode": "local", }, ) r = c.get("/admin/tables", headers=auth) body = r.text # The manageAccess() helper exists in the JS. assert "function manageAccess(" in body or "manageAccess =" in body # It targets the access page (the renderer ships the call site). assert "/admin/access" in body # Renderer emits the per-row Manage access button. assert 'title="Manage access"' in body assert "manageAccess(" in body def test_admin_access_supports_deep_link_for_table(seeded_app): """Phase O: /admin/access page reads a deep link from the URL on load so /admin/tables's per-row Manage access button lands the operator on a pre-filtered view of the picked table.""" c = seeded_app["client"] token = seeded_app["admin_token"] r = c.get("/admin/access", headers={"Authorization": f"Bearer {token}"}) body = r.text # The page reads window.location.hash on load and dispatches by prefix. assert "location.hash" in body and "table:" in body, \ "/admin/access must read the deep-link from URL on load"