From d3165d1e89f1c6b45403889e31b1fa1a170dbf57 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2026 18:59:30 +0530 Subject: [PATCH 1/5] test(locators): use case detail dashboards section and iframe locators --- locators/consumer/usecase_locators.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/locators/consumer/usecase_locators.py b/locators/consumer/usecase_locators.py index 54f9631..6bab3b3 100644 --- a/locators/consumer/usecase_locators.py +++ b/locators/consumer/usecase_locators.py @@ -24,3 +24,9 @@ class UseCaseLocators: # Scoped to /download/resource/ so it targets the dataset file, not the # chart-image link (/download/chart/), which is a separate failing endpoint. DOWNLOAD_LINK = (By.XPATH, "//a[contains(@href, '/download/resource/')]") + + # Use case detail page (/usecases/): dashboards embedded as iframes (DataSpaceFrontend #476) + DETAIL_DATASETS_HEADING = (By.XPATH, "//main//*[normalize-space(text())='Datasets in this Use Case']") + DETAIL_DASHBOARDS_HEADING = (By.XPATH, "//main//*[normalize-space(text())='Dashboards Linked to this Use Case']") + DETAIL_DASHBOARD_IFRAME = (By.XPATH, "//main//iframe") + DETAIL_DASHBOARD_OPEN_LINK = (By.XPATH, "//main//a[normalize-space()='Open dashboard in a new tab']") From ac05c325ba62ae8ab1cb70271fe1673c271b6af5 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2026 18:59:30 +0530 Subject: [PATCH 2/5] test(pages): UseCasePage detail helpers for embedded dashboards --- pages/consumer/usecase_page.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pages/consumer/usecase_page.py b/pages/consumer/usecase_page.py index 03f3ea5..f7dab3b 100644 --- a/pages/consumer/usecase_page.py +++ b/pages/consumer/usecase_page.py @@ -4,6 +4,8 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC import requests +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.support.ui import WebDriverWait from pages.base_page import BasePage from locators.consumer.usecase_locators import UseCaseLocators @@ -47,5 +49,40 @@ def download_first_associated_dataset(self, usecase_index: int = 0, dataset_inde status = requests.head(href, allow_redirects=True, timeout=10).status_code return (href, status) + # ── Use case detail page ───────────────────────────────────────────────── + def open_detail(self, base_url: str, usecase_id): + """Open /usecases/ and wait for the always-rendered datasets heading.""" + self.visit(f"{base_url.rstrip('/')}/usecases/{usecase_id}") + self.find(UseCaseLocators.DETAIL_DATASETS_HEADING) + return self + def has_dashboards_section(self, timeout: int = 10) -> bool: + try: + WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located(UseCaseLocators.DETAIL_DASHBOARDS_HEADING) + ) + return True + except TimeoutException: + return False + + def embedded_dashboards(self): + """[{title, src}] for each dashboard iframe on the detail page.""" + frames = self.finds(UseCaseLocators.DETAIL_DASHBOARD_IFRAME) + return [{"title": f.get_attribute("title"), "src": f.get_attribute("src")} for f in frames] + + def dashboard_open_links(self): + """[{href, target, rel}] for each 'Open dashboard in a new tab' link.""" + links = self.finds(UseCaseLocators.DETAIL_DASHBOARD_OPEN_LINK) + return [ + {"href": a.get_attribute("href"), "target": a.get_attribute("target"), "rel": a.get_attribute("rel")} + for a in links + ] + + def dashboards_render_before_datasets(self) -> bool: + dash = self.find(UseCaseLocators.DETAIL_DASHBOARDS_HEADING) + datasets = self.find(UseCaseLocators.DETAIL_DATASETS_HEADING) + return self.driver.execute_script( + "return !!(arguments[0].compareDocumentPosition(arguments[1]) & Node.DOCUMENT_POSITION_FOLLOWING);", + dash, datasets, + ) From e107e12a9f1ec0a0a6e64fdd1ce06bde0e03d5af Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2026 18:59:30 +0530 Subject: [PATCH 3/5] test(consumer): smoke coverage for embedded use case dashboards (DataSpaceFrontend #476) --- .../smoke/test_con_usecase_dashboards.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 tests/consumer/smoke/test_con_usecase_dashboards.py diff --git a/tests/consumer/smoke/test_con_usecase_dashboards.py b/tests/consumer/smoke/test_con_usecase_dashboards.py new file mode 100644 index 0000000..0edd733 --- /dev/null +++ b/tests/consumer/smoke/test_con_usecase_dashboards.py @@ -0,0 +1,110 @@ +# tests/consumer/smoke/test_con_usecase_dashboards.py +# +# Use case detail page dashboards (DataSpaceFrontend PR #476): linked +# dashboards are embedded as iframes above "Datasets in this Use Case", +# Superset links get standalone=1, and links that aren't http(s) are dropped +# (hiding the whole section when none are left). +# +# Use cases are picked from the backend at runtime rather than hardcoded, +# so the same tests work against dev and prod data. + +import os +from urllib.parse import parse_qs, urlsplit + +import pytest +import requests + +from pages.consumer.usecase_page import UseCasePage + +pytestmark = [pytest.mark.smoke, pytest.mark.readonly] + +BASE_URL = os.getenv("HOME_URL_DEV", "https://dev.civicdataspace.in") + + +def _gql(api, query): + r = requests.post(f"{api.rstrip('/')}/api/graphql", json={"query": query}, timeout=20) + r.raise_for_status() + body = r.json() + assert not body.get("errors"), body["errors"] + return body["data"] + + +def _published_ids(api): + return [u["id"] for u in _gql(api, "{ useCases(pagination:{limit:500}){ id status } }")["useCases"] + if u["status"] == "PUBLISHED"] + + +def _is_web_url(link): + return urlsplit(link or "").scheme in ("http", "https") + + +@pytest.fixture(scope="module") +def dashboards_by_usecase(): + api = os.getenv("API_BASE_URL") + if not api: + pytest.skip("API_BASE_URL not set: cannot find use cases with dashboards") + found = {} + for uc_id in _published_ids(api): + dash = _gql(api, "{ usecaseDashboards(usecaseId:%s){ id name link } }" % uc_id)["usecaseDashboards"] + if dash: + found[uc_id] = dash + return found + + +@pytest.fixture(scope="module") +def superset_usecase(dashboards_by_usecase): + for uc_id, dash in dashboards_by_usecase.items(): + superset = [d for d in dash if _is_web_url(d["link"]) and "/superset/" in urlsplit(d["link"]).path] + if superset: + return uc_id, superset[0] + pytest.skip("No published use case links a Superset dashboard") + + +@pytest.fixture +def uc_page(driver): + return UseCasePage(driver, timeout=30) + + +def test_superset_dashboard_is_embedded_standalone(uc_page, superset_usecase): + uc_id, dashboard = superset_usecase + uc_page.open_detail(BASE_URL, uc_id) + + frames = {f["title"]: f["src"] for f in uc_page.embedded_dashboards()} + assert dashboard["name"] in frames, f"use case {uc_id}: no iframe titled {dashboard['name']!r}, got {list(frames)}" + + src, link = urlsplit(frames[dashboard["name"]]), urlsplit(dashboard["link"]) + assert (src.netloc, src.path) == (link.netloc, link.path), f"iframe src {src.geturl()} is not the dashboard {link.geturl()}" + params = parse_qs(src.query) + assert params.get("standalone") == ["1"], f"Superset iframe src lacks standalone=1: {src.geturl()}" + for key, value in parse_qs(link.query).items(): + assert params.get(key) == value, f"iframe src dropped/changed ?{key}= from the dashboard link: {src.geturl()}" + + opens = [o for o in uc_page.dashboard_open_links() if o["href"] == src.geturl()] + assert opens, f"no 'Open dashboard in a new tab' link pointing at {src.geturl()}" + assert opens[0]["target"] == "_blank" + assert "noreferrer" in (opens[0]["rel"] or "") + + +def test_dashboards_render_above_datasets(uc_page, superset_usecase): + uc_id, _ = superset_usecase + uc_page.open_detail(BASE_URL, uc_id) + assert uc_page.has_dashboards_section(), f"use case {uc_id}: dashboards section missing" + assert uc_page.dashboards_render_before_datasets(), ( + f"use case {uc_id}: 'Dashboards Linked to this Use Case' renders below 'Datasets in this Use Case'" + ) + + +@pytest.mark.parametrize("case", ["no_dashboards", "no_usable_link"]) +def test_section_hidden_without_embeddable_dashboards(uc_page, dashboards_by_usecase, case): + if case == "no_dashboards": + ids = _published_ids(os.environ["API_BASE_URL"]) + uc_id = next((u for u in ids if u not in dashboards_by_usecase), None) + else: + uc_id = next((u for u, dash in dashboards_by_usecase.items() + if not any(_is_web_url(d["link"]) for d in dash)), None) + if uc_id is None: + pytest.skip(f"No published use case for case {case!r}") + uc_page.open_detail(BASE_URL, uc_id) + assert not uc_page.has_dashboards_section(timeout=8), ( + f"use case {uc_id} ({case}): dashboards section rendered with nothing embeddable to show" + ) From 2eaafbe8b556c72c0d40e16f724782646b32d788 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2026 18:59:30 +0530 Subject: [PATCH 4/5] ci: pass API_BASE_URL to consumer-smoke for dashboard use case lookup --- .github/workflows/run-smoke.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-smoke.yml b/.github/workflows/run-smoke.yml index 91ae8c1..06a2eeb 100644 --- a/.github/workflows/run-smoke.yml +++ b/.github/workflows/run-smoke.yml @@ -85,6 +85,9 @@ jobs: # Keycloak realm that dev and prod share. TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }} TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }} + # test_con_usecase_dashboards looks up which use cases link dashboards. + # Same resolution as api-smoke, so a prod readonly run reads prod data. + API_BASE_URL: ${{ inputs.api_base_url || vars.API_BASE_URL || 'https://dev.api.civicdataspace.in' }} steps: - uses: actions/checkout@v4 From 3035a35c37527f121be2f1521bd8cf8ec71acb90 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2026 19:07:09 +0530 Subject: [PATCH 5/5] test(consumer): prefer a Superset link without standalone=1 so the added param is actually proven --- tests/consumer/smoke/test_con_usecase_dashboards.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/consumer/smoke/test_con_usecase_dashboards.py b/tests/consumer/smoke/test_con_usecase_dashboards.py index 0edd733..b4b0f8f 100644 --- a/tests/consumer/smoke/test_con_usecase_dashboards.py +++ b/tests/consumer/smoke/test_con_usecase_dashboards.py @@ -53,11 +53,12 @@ def dashboards_by_usecase(): @pytest.fixture(scope="module") def superset_usecase(dashboards_by_usecase): - for uc_id, dash in dashboards_by_usecase.items(): - superset = [d for d in dash if _is_web_url(d["link"]) and "/superset/" in urlsplit(d["link"]).path] - if superset: - return uc_id, superset[0] - pytest.skip("No published use case links a Superset dashboard") + superset = [(uc_id, d) for uc_id, dash in dashboards_by_usecase.items() for d in dash + if _is_web_url(d["link"]) and "/superset/" in urlsplit(d["link"]).path] + if not superset: + pytest.skip("No published use case links a Superset dashboard") + # Prefer a link without standalone=1 so the test proves the frontend adds it. + return next((s for s in superset if "standalone" not in parse_qs(urlsplit(s[1]["link"]).query)), superset[0]) @pytest.fixture