mirror of
https://github.com/MinaSaad1/pbi-cli
synced 2026-04-21 13:37:19 +00:00
19 files from earlier v3 sessions were created on disk but never staged/committed: 11 source modules (backends, preview, templates, utils) and 8 test files (488 tests depend on these). Without these files the package would be broken on install. Found during pre-publish review.
340 lines
12 KiB
Python
340 lines
12 KiB
Python
"""Tests for visual calculation functions in pbi_cli.core.visual_backend.
|
|
|
|
Covers visual_calc_add, visual_calc_list, visual_calc_delete against a minimal
|
|
in-memory PBIR directory tree.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from pbi_cli.core.errors import PbiCliError
|
|
from pbi_cli.core.visual_backend import (
|
|
visual_calc_add,
|
|
visual_calc_delete,
|
|
visual_calc_list,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
|
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
|
|
|
|
def _read_json(path: Path) -> dict[str, Any]:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
@pytest.fixture
|
|
def visual_on_page(tmp_path: Path) -> tuple[Path, str, str]:
|
|
"""Build a minimal PBIR definition folder with one page and one visual.
|
|
|
|
Returns (definition_path, page_name, visual_name).
|
|
|
|
The visual has a minimal barChart structure with an empty Y queryState role.
|
|
"""
|
|
definition = tmp_path / "definition"
|
|
definition.mkdir()
|
|
|
|
pages_dir = definition / "pages"
|
|
pages_dir.mkdir()
|
|
|
|
page_dir = pages_dir / "test_page"
|
|
page_dir.mkdir()
|
|
|
|
visuals_dir = page_dir / "visuals"
|
|
visuals_dir.mkdir()
|
|
|
|
visual_dir = visuals_dir / "myvisual"
|
|
visual_dir.mkdir()
|
|
|
|
_write_json(
|
|
visual_dir / "visual.json",
|
|
{
|
|
"name": "myvisual",
|
|
"position": {"x": 0, "y": 0, "width": 400, "height": 300, "z": 0},
|
|
"visual": {
|
|
"visualType": "barChart",
|
|
"query": {
|
|
"queryState": {
|
|
"Y": {
|
|
"projections": [
|
|
{
|
|
"field": {
|
|
"Measure": {
|
|
"Expression": {"SourceRef": {"Entity": "Sales"}},
|
|
"Property": "Amount",
|
|
}
|
|
},
|
|
"queryRef": "Sales.Amount",
|
|
"nativeQueryRef": "Amount",
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
return definition, "test_page", "myvisual"
|
|
|
|
|
|
def _vfile(definition: Path, page: str, visual: str) -> Path:
|
|
return definition / "pages" / page / "visuals" / visual / "visual.json"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. visual_calc_add -- adds projection to role
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_add_appends_projection(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_add appends a NativeVisualCalculation projection to the role."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sum of Sales])")
|
|
|
|
data = _read_json(_vfile(definition, page, visual))
|
|
projections = data["visual"]["query"]["queryState"]["Y"]["projections"]
|
|
# Original measure projection plus the new calc
|
|
assert len(projections) == 2
|
|
last = projections[-1]
|
|
assert "NativeVisualCalculation" in last["field"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Correct NativeVisualCalculation structure
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_add_correct_structure(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""Added projection has correct NativeVisualCalculation fields."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(
|
|
definition, page, visual, "Running sum", "RUNNINGSUM([Sum of Sales])", role="Y"
|
|
)
|
|
|
|
data = _read_json(_vfile(definition, page, visual))
|
|
projections = data["visual"]["query"]["queryState"]["Y"]["projections"]
|
|
nvc_proj = next(
|
|
p for p in projections if "NativeVisualCalculation" in p.get("field", {})
|
|
)
|
|
nvc = nvc_proj["field"]["NativeVisualCalculation"]
|
|
|
|
assert nvc["Language"] == "dax"
|
|
assert nvc["Expression"] == "RUNNINGSUM([Sum of Sales])"
|
|
assert nvc["Name"] == "Running sum"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. queryRef is "select", nativeQueryRef equals calc_name
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_add_query_refs(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""queryRef is always 'select' and nativeQueryRef equals the calc name."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "My Calc", "RANK()")
|
|
|
|
data = _read_json(_vfile(definition, page, visual))
|
|
projections = data["visual"]["query"]["queryState"]["Y"]["projections"]
|
|
nvc_proj = next(
|
|
p for p in projections if "NativeVisualCalculation" in p.get("field", {})
|
|
)
|
|
|
|
assert nvc_proj["queryRef"] == "select"
|
|
assert nvc_proj["nativeQueryRef"] == "My Calc"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. visual_calc_list returns [] before any calcs added
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_list_empty_before_add(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_list returns an empty list when no calcs have been added."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
result = visual_calc_list(definition, page, visual)
|
|
|
|
assert result == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. visual_calc_list returns 1 item after add
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_list_one_after_add(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_list returns exactly one item after adding one calculation."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sales])")
|
|
result = visual_calc_list(definition, page, visual)
|
|
|
|
assert len(result) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. visual_calc_list returns correct name/expression/role
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_list_correct_fields(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_list returns correct name, expression, role, and query_ref."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sales])", role="Y")
|
|
result = visual_calc_list(definition, page, visual)
|
|
|
|
assert len(result) == 1
|
|
item = result[0]
|
|
assert item["name"] == "Running sum"
|
|
assert item["expression"] == "RUNNINGSUM([Sales])"
|
|
assert item["role"] == "Y"
|
|
assert item["query_ref"] == "select"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. visual_calc_add is idempotent (same name replaces, not duplicates)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_add_idempotent(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""Adding a calc with the same name replaces the existing one, not duplicates."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sales])")
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Revenue])")
|
|
|
|
result = visual_calc_list(definition, page, visual)
|
|
|
|
# Still exactly one NativeVisualCalculation named "Running sum"
|
|
running_sum_items = [r for r in result if r["name"] == "Running sum"]
|
|
assert len(running_sum_items) == 1
|
|
assert running_sum_items[0]["expression"] == "RUNNINGSUM([Revenue])"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. visual_calc_add to non-existent role creates the role
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_add_creates_new_role(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""Adding a calc to a role that does not exist creates that role."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "My Rank", "RANK()", role="Values")
|
|
|
|
data = _read_json(_vfile(definition, page, visual))
|
|
assert "Values" in data["visual"]["query"]["queryState"]
|
|
projections = data["visual"]["query"]["queryState"]["Values"]["projections"]
|
|
assert len(projections) == 1
|
|
assert "NativeVisualCalculation" in projections[0]["field"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Two different calcs: list returns 2
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_add_two_calcs_list_returns_two(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""Adding two distinct calcs results in two items returned by calc-list."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sales])")
|
|
visual_calc_add(definition, page, visual, "Rank", "RANK()")
|
|
|
|
result = visual_calc_list(definition, page, visual)
|
|
|
|
assert len(result) == 2
|
|
names = {r["name"] for r in result}
|
|
assert names == {"Running sum", "Rank"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. visual_calc_delete removes the projection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_delete_removes_projection(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_delete removes the named NativeVisualCalculation projection."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sales])")
|
|
visual_calc_delete(definition, page, visual, "Running sum")
|
|
|
|
data = _read_json(_vfile(definition, page, visual))
|
|
projections = data["visual"]["query"]["queryState"]["Y"]["projections"]
|
|
nvc_projections = [
|
|
p for p in projections if "NativeVisualCalculation" in p.get("field", {})
|
|
]
|
|
assert nvc_projections == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 11. visual_calc_delete raises PbiCliError for unknown name
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_delete_raises_for_unknown_name(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_delete raises PbiCliError when the calc name does not exist."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
with pytest.raises(PbiCliError, match="not found"):
|
|
visual_calc_delete(definition, page, visual, "Nonexistent Calc")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 12. visual_calc_list after delete returns N-1
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_visual_calc_list_after_delete_returns_n_minus_one(
|
|
visual_on_page: tuple[Path, str, str],
|
|
) -> None:
|
|
"""visual_calc_list returns N-1 items after deleting one of N calcs."""
|
|
definition, page, visual = visual_on_page
|
|
|
|
visual_calc_add(definition, page, visual, "Running sum", "RUNNINGSUM([Sales])")
|
|
visual_calc_add(definition, page, visual, "Rank", "RANK()")
|
|
|
|
assert len(visual_calc_list(definition, page, visual)) == 2
|
|
|
|
visual_calc_delete(definition, page, visual, "Running sum")
|
|
result = visual_calc_list(definition, page, visual)
|
|
|
|
assert len(result) == 1
|
|
assert result[0]["name"] == "Rank"
|