DataDesigner/packages/data-designer/tests/cli/commands/test_preview_command.py
Johnny Greco f2a1657870
feat: add --save-results option to preview command (#333)
* feat: add --save-report option to preview command

* feat: add save_path option to display_sample_record

Allow saving rendered sample records as HTML or SVG files via an
optional save_path parameter on both the standalone function and
the WithRecordSamplerMixin method.

* feat: replace --save-report with --save-results on preview command

Replace the single-file --save-report option with --save-results, which saves all preview artifacts (dataset parquet, analysis report HTML, and per-record sample HTMLs) into a timestamped directory under the artifact path. Add error handling around the save block, improve timestamp precision to microseconds, and expand test coverage for the new behavior.

* feat: add sample records pager with theme toggle, postMessage bridge, and UI polish

* feat: add dataset metadata subtitle to pager and clean up toolbar layout

* fix: address review findings for preview save-results feature

- Split try/except in generation_controller so report display errors
  don't produce misleading "failed to save" messages when not saving
- Add browser HTML path to save success output for discoverability
- Remove 5 unused CSS variables from pager theme constants
- Add "N of M" record counter to pager toolbar
- Add theme/display_width assertions to all preview_command tests
- Add dedicated test for custom theme and display_width passthrough
- Add tests for record counter and CSS variable cleanup

* fix: address code review findings and simplify pager

- Fix critical bug: analysis report now displays to console even when
  --save-results is active (was silently dropped via pass statement)
- Fix latent UnboundLocalError in display_sample_record when index is
  out of bounds (num_records computed before try block)
- Eliminate duplicated dark CSS between constant and theme listener script
- Simplify sample_records_pager: remove dual-theme system, postMessage
  bridge, and responsive media queries; restore GitHub link; reorder
  toolbar to put prev/next buttons on the far left
- Narrow except Exception to except OSError in save-results path
- Use case-insensitive extension check and lambda-based re.sub
- Collapse redundant preview command delegation tests into parametrize
- Add missing type annotations and remove tautological assertions

* style: move record counter to far right of pager toolbar

* refactor: remove dead theme-listener script and inline CSS constant

_THEME_LISTENER_SCRIPT and _SAMPLE_RECORD_DARK_CSS_INLINE became
orphaned after the pager simplification removed the postMessage
bridge. This removes both constants, drops the injection line,
switches the idempotency guard to the viewport meta tag, and
cleans up related test assertions.

* fix: move Path import out of TYPE_CHECKING block in test_visualization

* fix: rename _logger to logger to match codebase convention

* fix: remove unnecessary cast in preview command theme parameter

* refactor: extract DEFAULT_DISPLAY_WIDTH constant and make apply_html_post_processing public

* Update packages/data-designer-config/tests/config/utils/test_visualization.py

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-18 15:58:35 -05:00

134 lines
4.3 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from data_designer.cli.commands.preview import preview_command
from data_designer.cli.ui import wait_for_navigation_key
# ---------------------------------------------------------------------------
# preview_command delegation tests
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"kwargs",
[
pytest.param(
{
"config_source": "config.yaml",
"num_records": 5,
"non_interactive": True,
"save_results": False,
"artifact_path": None,
"theme": "dark",
"display_width": 110,
},
id="defaults",
),
pytest.param(
{
"config_source": "config.yaml",
"num_records": 10,
"non_interactive": False,
"save_results": False,
"artifact_path": None,
"theme": "dark",
"display_width": 110,
},
id="non_interactive_false",
),
pytest.param(
{
"config_source": "my_config.py",
"num_records": 20,
"non_interactive": True,
"save_results": False,
"artifact_path": None,
"theme": "dark",
"display_width": 110,
},
id="custom_num_records",
),
pytest.param(
{
"config_source": "config.yaml",
"num_records": 5,
"non_interactive": True,
"save_results": True,
"artifact_path": "/custom/output",
"theme": "dark",
"display_width": 110,
},
id="save_results_and_artifact_path",
),
pytest.param(
{
"config_source": "config.yaml",
"num_records": 5,
"non_interactive": True,
"save_results": True,
"artifact_path": None,
"theme": "light",
"display_width": 80,
},
id="custom_theme_and_display_width",
),
],
)
@patch("data_designer.cli.commands.preview.GenerationController")
def test_preview_command_delegates_to_controller(mock_ctrl_cls: MagicMock, kwargs: dict[str, object]) -> None:
"""Test preview_command delegates all arguments to GenerationController.run_preview."""
mock_ctrl = MagicMock()
mock_ctrl_cls.return_value = mock_ctrl
preview_command(**kwargs)
mock_ctrl_cls.assert_called_once()
mock_ctrl.run_preview.assert_called_once_with(**kwargs)
# ---------------------------------------------------------------------------
# wait_for_navigation_key unit tests (UI-layer, unchanged by refactor)
# ---------------------------------------------------------------------------
@patch("data_designer.cli.ui.Application")
def test_wait_for_navigation_key_creates_app_and_runs(mock_app_cls: MagicMock) -> None:
"""Test wait_for_navigation_key creates an Application and calls run()."""
mock_app_instance = MagicMock()
mock_app_cls.return_value = mock_app_instance
result = wait_for_navigation_key()
mock_app_cls.assert_called_once()
mock_app_instance.run.assert_called_once()
assert result == "q"
@patch("data_designer.cli.ui.Application")
def test_wait_for_navigation_key_handles_keyboard_interrupt(mock_app_cls: MagicMock) -> None:
"""Test wait_for_navigation_key returns 'q' on KeyboardInterrupt."""
mock_app_instance = MagicMock()
mock_app_cls.return_value = mock_app_instance
mock_app_instance.run.side_effect = KeyboardInterrupt
result = wait_for_navigation_key()
assert result == "q"
@patch("data_designer.cli.ui.Application")
def test_wait_for_navigation_key_handles_eof_error(mock_app_cls: MagicMock) -> None:
"""Test wait_for_navigation_key returns 'q' on EOFError."""
mock_app_instance = MagicMock()
mock_app_cls.return_value = mock_app_instance
mock_app_instance.run.side_effect = EOFError
result = wait_for_navigation_key()
assert result == "q"