Skip to content

Commit

Permalink
Merge pull request #51 from Carifio24/dialog-tests
Browse files Browse the repository at this point in the history
Add tests of export tools and dialogs
  • Loading branch information
Carifio24 authored Sep 12, 2024
2 parents ffe2743 + 0e6c8f6 commit a619ea2
Show file tree
Hide file tree
Showing 13 changed files with 646 additions and 27 deletions.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
recursive-include glue_ar/js *
recursive-include glue_ar/resources *
include **/*.vue
include **/*.ui
106 changes: 106 additions & 0 deletions glue_ar/common/tests/test_base_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from random import random, seed
from typing import Any

from echo import CallbackProperty
from glue.core import Application, Data
from glue.core.link_helpers import LinkSame
from glue.core.state_objects import State
from numpy import arange, ones


class DummyState(State):
cb_int = CallbackProperty(0)
cb_float = CallbackProperty(1.7)
cb_bool = CallbackProperty(False)


class BaseExportDialogTest:

app: Application
dialog: Any

def _setup_data(self):
seed(102)
self.volume_data = Data(
label='Volume Data',
x=arange(24).reshape((2, 3, 4)),
y=ones((2, 3, 4)),
z=arange(100, 124).reshape((2, 3, 4)))
self.app.data_collection.append(self.volume_data)

scatter_size = 50
self.scatter_data = Data(x=[random() for _ in range(scatter_size)],
y=[random() for _ in range(scatter_size)],
z=[random() for _ in range(scatter_size)],
label="Scatter Data")
self.app.data_collection.append(self.scatter_data)

# Link pixel axes to scatter
for i, c in enumerate(('x', 'y', 'z')):
ri = 2 - i
c1 = self.volume_data.id[f"Pixel Axis {ri} [{c}]"]
c2 = self.scatter_data.id[c]
self.app.data_collection.add_link(LinkSame(c1, c2))

def test_default_state(self):
state = self.dialog.state
assert state.filetype == "glB"
assert state.compression == "None"
assert state.layer == "Volume Data"
assert state.method in {"Isosurface", "Voxel"}

assert state.filetype_helper.choices == ['glB', 'glTF', 'USDC', 'USDA']
assert state.compression_helper.choices == ['None', 'Draco', 'Meshoptimizer']
assert state.layer_helper.choices == ["Volume Data", "Scatter Data"]
assert set(state.method_helper.choices) == {"Isosurface", "Voxel"}

def test_default_dictionary(self):
state_dict = self.dialog.state_dictionary
assert len(state_dict) == 2
assert set(state_dict.keys()) == {"Volume Data", "Scatter Data"}

def test_layer_change_state(self):
state = self.dialog.state

state.layer = "Scatter Data"
assert state.method_helper.choices == ["Scatter"]
assert state.method == "Scatter"

state.layer = "Volume Data"
assert set(state.method_helper.choices) == {"Isosurface", "Voxel"}
assert state.method in {"Isosurface", "Voxel"}

state.layer = "Scatter Data"
assert state.method_helper.choices == ["Scatter"]
assert state.method == "Scatter"

def test_method_settings_persistence(self):
state = self.dialog.state

state.layer = "Volume Data"
state.method = "Voxel"
method, layer_export_state = self.dialog.state_dictionary["Volume Data"]
assert method == "Voxel"
layer_export_state.opacity_cutoff = 0.5

state.method = "Isosurface"
method, layer_export_state = self.dialog.state_dictionary["Volume Data"]
layer_export_state.isosurface_count = 25

state.method = "Voxel"
method, layer_export_state = self.dialog.state_dictionary["Volume Data"]
assert method == "Voxel"
assert layer_export_state.opacity_cutoff == 0.5

state.layer = "Scatter Data"

state.layer = "Volume Data"
state.method = "Voxel"
method, layer_export_state = self.dialog.state_dictionary["Volume Data"]
assert method == "Voxel"
assert layer_export_state.opacity_cutoff == 0.5

state.method = "Isosurface"
method, layer_export_state = self.dialog.state_dictionary["Volume Data"]
assert method == "Isosurface"
assert layer_export_state.isosurface_count == 25
13 changes: 4 additions & 9 deletions glue_ar/jupyter/export_dialog.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import ipyvuetify as v # noqa
from ipyvuetify.VuetifyTemplate import VuetifyTemplate
from ipywidgets import DOMWidget, VBox, widget_serialization
from ipywidgets import DOMWidget, widget_serialization
import traitlets
from typing import Callable, List, Optional

from echo import HasCallbackProperties
from glue.core.state_objects import State
from glue.viewers.common.viewer import Viewer
from glue_jupyter.link import link
from glue_jupyter.state_traitlets_helpers import GlueState
from glue_jupyter.vuetify_helpers import link_glue_choices

from glue_ar.common.export_dialog_base import ARExportDialogBase
Expand Down Expand Up @@ -51,7 +50,6 @@ def vue_temp_rule(self, value):
class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate):

template_file = (__file__, "export_dialog.vue")
dialog_state = GlueState().tag(sync=True)
dialog_open = traitlets.Bool().tag(sync=True)

layer_items = traitlets.List().tag(sync=True)
Expand All @@ -67,7 +65,7 @@ class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate):
method_items = traitlets.List().tag(sync=True)
method_selected = traitlets.Int().tag(sync=True)

layer_layout = traitlets.Instance(DOMWidget).tag(sync=True, **widget_serialization)
layer_layout = traitlets.Instance(v.Container).tag(sync=True, **widget_serialization)
has_layer_options = traitlets.Bool().tag(sync=True)

def __init__(self,
Expand All @@ -76,7 +74,7 @@ def __init__(self,
on_cancel: Optional[Callable] = None,
on_export: Optional[Callable] = None):
ARExportDialogBase.__init__(self, viewer=viewer)
self.layer_layout = VBox()
self.layer_layout = v.Container()
VuetifyTemplate.__init__(self)

self._on_layer_change(self.state.layer)
Expand All @@ -89,7 +87,6 @@ def __init__(self,
self.dialog_open = display
self.on_cancel = on_cancel
self.on_export = on_export
self.dialog_state = self.state

self.input_widgets = []

Expand Down Expand Up @@ -123,14 +120,13 @@ def widgets_for_property(self,

value = getattr(instance, property)
t = type(value)
prop_name = self.display_name(property)
if t is bool:
widget = v.Checkbox(label=display_name)
link((instance, property), (widget, 'value'))
return [widget]
elif t in (int, float):
name = "integer" if t is int else "number"
widget = NumberField(type=t, label=prop_name, error_message=f"You must enter a valid {name}")
widget = NumberField(type=t, label=display_name, error_message=f"You must enter a valid {name}")
link((instance, property),
(widget, 'value'),
lambda value: str(value),
Expand All @@ -140,7 +136,6 @@ def widgets_for_property(self,
return []

def vue_cancel_dialog(self, *args):
self.state = None
self.state_dictionary = {}
self.dialog_open = False
if self.on_cancel:
Expand Down
Empty file.
141 changes: 141 additions & 0 deletions glue_ar/jupyter/tests/test_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from unittest.mock import MagicMock
from typing import cast

from glue_jupyter import JupyterApplication
# We can't use the Jupyter vispy widget for these tests until
# https://github.com/glue-viz/glue-vispy-viewers/pull/388 is released
from glue_jupyter.ipyvolume.volume import IpyvolumeVolumeView
from ipyvuetify import Checkbox

from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState
from glue_ar.jupyter.export_dialog import JupyterARExportDialog, NumberField


class TestJupyterExportDialog(BaseExportDialogTest):

app: JupyterApplication
dialog: JupyterARExportDialog

def setup_method(self, method):
self.app = JupyterApplication()
self._setup_data()

# We use a volume viewer because it can support both volume and scatter layers
self.viewer: IpyvolumeVolumeView = cast(IpyvolumeVolumeView,
self.app.volshow(widget="ipyvolume", data=self.volume_data))
self.viewer.add_data(self.scatter_data)

self.on_cancel = MagicMock()
self.on_export = MagicMock()
self.dialog = JupyterARExportDialog(viewer=self.viewer, display=True,
on_cancel=self.on_cancel, on_export=self.on_export)

def teardown_method(self, method):
self.dialog.dialog_open = False

def test_default_ui(self):
assert self.dialog.dialog_open
assert self.dialog.layer_items == [
{"text": "Volume Data", "value": 0},
{"text": "Scatter Data", "value": 1}
]
assert self.dialog.layer_selected == 0
assert self.dialog.compression_items == [
{"text": "None", "value": 0},
{"text": "Draco", "value": 1},
{"text": "Meshoptimizer", "value": 2}
]
assert self.dialog.compression_selected == 0
assert self.dialog.filetype_items == [
{"text": "glB", "value": 0},
{"text": "glTF", "value": 1},
{"text": "USDC", "value": 2},
{"text": "USDA", "value": 3}
]
assert self.dialog.filetype_selected == 0
assert set([item["text"] for item in self.dialog.method_items]) == {"Isosurface", "Voxel"}
assert self.dialog.method_selected == 0
assert self.dialog.has_layer_options

def test_filetype_change(self):
state = self.dialog.state

state.filetype = "USDC"
assert not self.dialog.show_compression

state.filetype = "USDA"
assert not self.dialog.show_compression

state.filetype = "glTF"
assert self.dialog.show_compression

state.filetype = "USDA"
assert not self.dialog.show_compression

state.filetype = "glB"
assert self.dialog.show_compression

state.filetype = "glTF"
assert self.dialog.show_compression

def test_widgets_for_property(self):
state = DummyState()

int_widgets = self.dialog.widgets_for_property(state, "cb_int", "Int CB")
assert len(int_widgets) == 1
widget = int_widgets[0]
assert isinstance(widget, NumberField)
assert widget.label == "Int CB"
assert widget.value == "0"
assert widget.number_type is int
assert widget.error_message == "You must enter a valid integer"

float_widgets = self.dialog.widgets_for_property(state, "cb_float", "Float CB")
assert len(float_widgets) == 1
widget = float_widgets[0]
assert isinstance(widget, NumberField)
assert widget.label == "Float CB"
assert widget.value == "1.7"
assert widget.number_type is float
assert widget.error_message == "You must enter a valid number"

bool_widgets = self.dialog.widgets_for_property(state, "cb_bool", "Bool CB")
assert len(bool_widgets) == 1
widget = bool_widgets[0]
assert isinstance(widget, Checkbox)
assert widget.label == "Bool CB"
assert widget.value is False

def test_update_layer_ui(self):
state = DummyState()
self.dialog._update_layer_ui(state)
assert len(self.dialog.layer_layout.children) == 3

def test_layer_change_ui(self):
state = self.dialog.state

state.layer = "Scatter Data"
assert self.dialog.method_selected == 0
assert self.dialog.method_items == [{"text": "Scatter", "value": 0}]
assert not self.dialog.has_layer_options

state.layer = "Volume Data"
assert self.dialog.method_items[self.dialog.method_selected]["text"] == state.method
assert set([item["text"] for item in self.dialog.method_items]) == {"Isosurface", "Voxel"}
assert self.dialog.has_layer_options

state.layer = "Scatter Data"
assert self.dialog.method_selected == 0
assert self.dialog.method_items == [{"text": "Scatter", "value": 0}]
assert not self.dialog.has_layer_options

def test_on_cancel(self):
self.dialog.vue_cancel_dialog()
assert len(self.dialog.state_dictionary) == 0
assert not self.dialog.dialog_open
self.on_cancel.assert_called_once_with()

def test_on_export(self):
self.dialog.vue_export_viewer()
assert not self.dialog.dialog_open
self.on_export.assert_called_once_with()
14 changes: 12 additions & 2 deletions glue_ar/qt/export_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from glue_qt.utils import load_ui
from glue_ar.common.export_dialog_base import ARExportDialogBase

from qtpy.QtWidgets import QCheckBox, QDialog, QHBoxLayout, QLabel, QLayout, QLineEdit, QWidget
from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLayout, QLineEdit, QWidget
from qtpy.QtGui import QIntValidator, QDoubleValidator


Expand Down Expand Up @@ -65,8 +65,19 @@ def _clear_layout(self, layout: QLayout):
else:
self._clear_layout(item.layout())

layout.removeItem(item)

if isinstance(layout, QFormLayout):
self._clear_form_rows(layout)

def _clear_form_rows(self, layout: QFormLayout):
if layout is not None:
while layout.rowCount():
layout.removeRow(0)

def _clear_layer_layout(self):
self._clear_layout(self.ui.layer_layout)
self._layer_connections = []

def _on_layer_change(self, layer_name: str):
super()._on_layer_change(layer_name)
Expand All @@ -76,7 +87,6 @@ def _on_layer_change(self, layer_name: str):

def _update_layer_ui(self, state: State):
self._clear_layer_layout()
self._layer_connections = []
for property in state.callback_properties():
row = QHBoxLayout()
name = self.display_name(property)
Expand Down
Loading

0 comments on commit a619ea2

Please sign in to comment.