pbi-cli/tests/test_visual_calc.py

333 lines
12 KiB
Python
Raw Permalink Normal View History

"""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"