Files
superset2/tests/unit_tests/widgets/test_api.py
T
Enzo MartellucciandClaude Sonnet 5 e9f7b7206a feat(dashboard-v2): add ad-hoc metric editing and redesign placement controls
Metrics fields now render ad-hoc aggregate/SQL entries as editable rows
via a new AdhocMetricEditor popover instead of falling back to raw JSON
for the whole field whenever any entry isn't a saved-metric string.
Dataset metadata fetch extended with column verbose names and the
disallow_adhoc_metrics flag needed to support it.

Also redesigns the Inspector's Placement section as a collapsed panel
with slider controls, and adds consistent field-row padding across the
panel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:34:58 +02:00

97 lines
3.6 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for ``WidgetControlsRestApi.validate`` — the REST boundary the
Dashboard V2 Inspector's form and JSON editor commit through (see
``controlValueValidation.ts``). It is a thin wrapper over
``Widget.validate_control_values``, the same call ``set_widget_control_values``
(the MCP write tool) commits through, so a frontend edit and an MCP edit are
validated by one implementation reached by two routes, not two rule sets that
can drift apart."""
from __future__ import annotations
from superset.utils import json
def test_validate_accepts_valid_control_values(client, full_api_access) -> None:
resp = client.post(
"/api/v1/widgets/type/metric-tile/validate",
data=json.dumps(
{
"control_values": {
"dataBinding": {"datasetId": 1, "metrics": ["count"]},
}
}
),
content_type="application/json",
)
assert resp.status_code == 200
assert resp.get_json()["result"]["errors"] == []
def test_validate_rejects_invalid_control_values(client, full_api_access) -> None:
resp = client.post(
"/api/v1/widgets/type/metric-tile/validate",
data=json.dumps({"control_values": {"decimals": "not-an-int"}}),
content_type="application/json",
)
assert resp.status_code == 200
errors = resp.get_json()["result"]["errors"]
locs = [tuple(error["loc"]) for error in errors]
# Missing the required dataBinding, and decimals is the wrong type: both
# of the widget's own model rules, not something this route invents.
assert ("dataBinding",) in locs
assert ("decimals",) in locs
def test_validate_unknown_widget_type_is_404(client, full_api_access) -> None:
resp = client.post(
"/api/v1/widgets/type/not-a-real-widget-type/validate",
data=json.dumps({"control_values": {}}),
content_type="application/json",
)
assert resp.status_code == 404
def test_validate_is_a_thin_wrapper_over_the_widget_s_own_validator(
client, full_api_access
) -> None:
"""The route's result must equal calling the widget's
``validate_control_values`` directly with the same input — proving the
REST boundary and the MCP ``set_widget_control_values`` tool (which also
calls ``validate_control_values`` directly) run through the identical
implementation rather than parallel copies of it."""
from superset.widgets.registry import registry
control_values = {"decimals": "not-an-int"}
widget = registry.get("metric-tile")
assert widget is not None
direct_errors = widget.validate_control_values(control_values)
resp = client.post(
"/api/v1/widgets/type/metric-tile/validate",
data=json.dumps({"control_values": control_values}),
content_type="application/json",
)
assert resp.status_code == 200
assert resp.get_json()["result"]["errors"] == direct_errors