Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import copy
import importlib
import sys

import pytest
from lfx.components.processing.data_operations import DataOperationsComponent
from lfx.components.processing.data_operations import ACTION_CONFIG, DataOperationsComponent
from lfx.custom.utils import build_custom_component_template
from lfx.graph.graph.base import Graph
from lfx.schema import Data

from tests.base import ComponentTestBaseWithoutClient
Expand Down Expand Up @@ -243,3 +246,113 @@ def test_update_build_config_clears_input_fields_when_operation_removed(self):
# Default fields (operations, data) should remain visible
assert result["operations"]["show"] is True
assert result["data"]["show"] is True


class TestUpdateFrontendNodeRestoresVisibility:
"""Clicking Update on a saved JSON Operations node must keep its operation's fields visible.

``update_frontend_node`` rebuilds the template from class defaults, where every operation field
has ``show=False``. The generic merge restores only values, so before the fix the selected
operation's input disappeared and ``ParameterHandler`` dropped it from the run.
"""

NODE_ID = "DataOperations-test"

@staticmethod
def _saved_node(operation: str | None, **values) -> dict:
"""Return a node as the canvas saves it after the user picks ``operation`` and fills ``values``."""
node, _ = build_custom_component_template(DataOperationsComponent(), user_id="test-user")
template = node["template"]
component = DataOperationsComponent()
component.update_build_config(template, [{"name": operation}] if operation else [], "operations")
for field, value in values.items():
template[field]["value"] = value
if field == "mapped_json_display":
component.update_build_config(template, value, field)
return node

@staticmethod
async def _update(saved: dict) -> dict:
"""Run the Update Component path: a fresh class-default node merged with the saved one."""
fresh, _ = build_custom_component_template(DataOperationsComponent(), user_id="test-user")
return await DataOperationsComponent().update_frontend_node(fresh, copy.deepcopy(saved))

def _run(self, node: dict, data: Data) -> Data:
"""Run ``node`` with the params a graph build hands the component, which skips hidden fields."""
payload = {
"nodes": [{"id": self.NODE_ID, "data": {"id": self.NODE_ID, "type": "DataOperations", "node": node}}],
"edges": [],
}
graph = Graph.from_payload(payload, flow_id="test-flow", user_id="test-user")
params = {k: v for k, v in graph.get_vertex(self.NODE_ID).params.items() if k != "code"}
return DataOperationsComponent(data=data, **params).as_data()

@staticmethod
def _visibility(template: dict) -> dict[str, bool]:
return {field: template[field]["show"] for field in DataOperationsComponent.ALL_OPERATION_FIELDS}

@pytest.mark.parametrize("operation", list(DataOperationsComponent.actions_data))
async def test_update_keeps_saved_visibility(self, operation):
saved = self._saved_node(operation)
template = (await self._update(saved))["template"]

shown = {field for field, show in self._visibility(template).items() if show}
assert shown == set(DataOperationsComponent.actions_data[operation]) - {"operations"}
assert self._visibility(template) == self._visibility(saved["template"])
assert template["data"]["show"] is True
assert template["operations"]["show"] is True
assert template["data"]["is_list"] is ACTION_CONFIG[operation]["is_list"]
assert template["operations"]["value"] == [{"name": operation}]

async def test_saved_select_keys_value_survives(self):
template = (await self._update(self._saved_node("Select Keys", select_keys_input=["text"])))["template"]
assert template["select_keys_input"]["show"] is True
assert template["select_keys_input"]["value"] == ["text"]
assert template["data"]["is_list"] is False

async def test_no_operation_hides_all_operation_fields(self):
template = (await self._update(self._saved_node(None)))["template"]
assert not any(self._visibility(template).values())
assert template["data"]["show"] is True

async def test_removed_operation_hides_all_operation_fields(self):
template = (await self._update(self._saved_node("Filter Values")))["template"]
assert not any(self._visibility(template).values())
assert template["data"]["show"] is True

async def test_path_selection_rebuilds_selected_key_options(self):
saved = self._saved_node("Path Selection", mapped_json_display='{"a": {"b": 1}}', selected_key=".a.b")
template = (await self._update(saved))["template"]
assert template["selected_key"]["show"] is True
assert template["selected_key"]["value"] == ".a.b"
assert template["selected_key"]["options"] == [".a", ".a.b"]

async def test_path_selection_with_invalid_saved_json_hides_selected_key(self):
saved = self._saved_node("Path Selection", mapped_json_display="{not json")
template = (await self._update(saved))["template"]
assert template["mapped_json_display"]["show"] is True
assert template["selected_key"]["show"] is False

@pytest.mark.parametrize(
("operation", "values", "expected"),
[
("Select Keys", {"select_keys_input": ["text"]}, {"text": "hello"}),
("Remove Keys", {"remove_keys_input": ["extra"]}, {"text": "hello"}),
("Rename Keys", {"rename_keys_input": {"text": "body"}}, {"body": "hello", "extra": 1}),
("Append or Update", {"append_update_data": {"tag": "new"}}, {"text": "hello", "extra": 1, "tag": "new"}),
("JQ Expression", {"query": ".text"}, {"result": "hello"}),
(
"Path Selection",
{"mapped_json_display": '{"text": "x", "extra": 0}', "selected_key": ".text"},
{"result": "hello"},
),
],
)
async def test_run_output_unchanged_after_update(self, operation, values, expected):
"""The smoke-test case: Select Keys ``['text']`` returned ``{}`` after Update."""
saved = self._saved_node(operation, **values)
updated = await self._update(saved)
data = Data(data={"text": "hello", "extra": 1})

assert self._run(saved, data).data == expected
assert self._run(updated, data).data == expected
Loading
Loading