Repository: feder-observatory/stellarphot

Issue:
Add a delete button for the saveable settings
I suppose errors will happen and it would be nice to be able to delete them.

Discussion:

Patch:
diff --git a/stellarphot/settings/custom_widgets.py b/stellarphot/settings/custom_widgets.py
index e50a9c3..46ff21a 100644
--- a/stellarphot/settings/custom_widgets.py
+++ b/stellarphot/settings/custom_widgets.py
@@ -14,6 +14,8 @@ from stellarphot.settings import (
     ui_generator,
 )
 
+DEFAULT_BUTTON_WIDTH = "300px"
+
 
 class ChooseOrMakeNew(ipw.VBox):
     """
@@ -52,6 +54,12 @@ class ChooseOrMakeNew(ipw.VBox):
         # keep track of whether we are editing an existing item
         self._editing = False
 
+        # also track whether we are in the midst of a delete confirmation
+        self._deleting = False
+
+        # and track if we are making a new item
+        self._making_new = False
+
         self._display_name = item_type_name.replace("_", " ")
 
         # Create the child widgets
@@ -61,27 +69,32 @@ class ChooseOrMakeNew(ipw.VBox):
 
         self._choose_existing = ipw.Dropdown(description="")
 
-        self._edit_button = ipw.Button(
-            description=f"Edit this {self._display_name}",
+        self._edit_delete_container = ipw.HBox(
             # width below was chosen to match the dropdown...would prefer to
             # determine this programmatically but don't know how.
-            layout={"width": "300px"},
+            layout={"width": DEFAULT_BUTTON_WIDTH}
+        )
+
+        self._edit_button = ipw.Button(
+            description=f"Edit this {self._display_name}",
         )
 
-        self._confirm_edit = Confirm(
-            message=f"Replace value of this {self._display_name}?",
+        self._delete_button = ipw.Button(
+            description=f"Delete this {self._display_name}",
         )
 
-        self._item_widget, self._widget_value_new_item = self._make_new_widget()
+        self._edit_delete_container.children = [self._edit_button, self._delete_button]
 
-        self._edit_button.on_click(self._edit_button_action)
+        self._confirm_edit_delete = Confirm()
+
+        self._item_widget, self._widget_value_new_item = self._make_new_widget()
 
         # Build the main widget
         self.children = [
             self._title,
             self._choose_existing,
-            self._edit_button,
-            self._confirm_edit,
+            self._edit_delete_container,
+            self._confirm_edit_delete,
             self._item_widget,
         ]
 
@@ -107,14 +120,20 @@ class ChooseOrMakeNew(ipw.VBox):
 
         # Set up some observers
 
+        # Respond to user clicking the edit button
+        self._edit_button.on_click(self._edit_button_action)
+
+        # Respond to user clicking the delete button
+        self._delete_button.on_click(self._delete_button_action)
+
         # Respond to user clicking the save button
         self._item_widget.savebuttonbar.fns_onsave_add_action(self._save_confirmation())
 
         # Respond to user interacting with a confirmation widget
         # Hide the save button bar so the user gets the confirmation instead
-        self._confirm_edit.widget_to_hide = self._item_widget.savebuttonbar
+        self._confirm_edit_delete.widget_to_hide = self._item_widget.savebuttonbar
         # Add the observer
-        self._confirm_edit.observe(self._handle_confirmation(), names="value")
+        self._confirm_edit_delete.observe(self._handle_confirmation(), names="value")
 
         # Respond when user wants to make a new thing
         self._choose_existing.observe(self._handle_selection, names="value")
@@ -130,7 +149,8 @@ class ChooseOrMakeNew(ipw.VBox):
             # we only want to ask for confirmation if we are editing an existing item
             # rather than saving a new one.
             if self._editing:
-                self._confirm_edit.show()
+                self._set_confirm_message()
+                self._confirm_edit_delete.show()
 
         return f
 
@@ -151,7 +171,7 @@ class ChooseOrMakeNew(ipw.VBox):
             # We are making a new item...
 
             # Hide the edit button
-            self._edit_button.layout.display = "none"
+            self._edit_delete_container.layout.display = "none"
 
             # This sets the ui back to its original state when created, i.e.
             # everything is empty.
@@ -180,6 +200,9 @@ class ChooseOrMakeNew(ipw.VBox):
             self._item_widget.open_nested = False
             self._item_widget.open_nested = True
 
+            # Note that we are making a new item
+            self._making_new = True
+
         else:
             # Display the selected item...
             self._item_widget.show_savebuttonbar = False
@@ -188,7 +211,7 @@ class ChooseOrMakeNew(ipw.VBox):
             self._item_widget.value = self._get_item(change["new"].name)
 
             # Display the edit button
-            self._edit_button.layout.display = "flex"
+            self._edit_delete_container.layout.display = "flex"
 
             # Really only applies to PassbandMap, which has nested models,
             # but does no harm in the other cases
@@ -199,7 +222,7 @@ class ChooseOrMakeNew(ipw.VBox):
         Handle the edit button being clicked.
         """
         # Replace the display of the edit button with the save button bar...
-        self._edit_button.layout.display = "none"
+        self._edit_delete_container.layout.display = "none"
         self._item_widget.show_savebuttonbar = True
         # ...enable the widget...
         self._item_widget.disabled = False
@@ -227,6 +250,20 @@ class ChooseOrMakeNew(ipw.VBox):
         # Enable the revert button so that the user can cancel the edit
         self._item_widget.savebuttonbar.bn_revert.disabled = False
 
+    def _delete_button_action(self, _):
+        """
+        Handle the delete button being clicked.
+        """
+        # Change our state
+        self._deleting = True
+
+        # Hide the edit/delete buttons
+        self._edit_delete_container.layout.display = "none"
+
+        # Show the confirmation widget
+        self._set_confirm_message()
+        self._confirm_edit_delete.show()
+
     def _set_disable_state_nested_models(self, top, value):
         """
         When a one model contains another and the top-level model widget
@@ -261,6 +298,17 @@ class ChooseOrMakeNew(ipw.VBox):
             # No children...
             pass
 
+    def _set_confirm_message(self):
+        """
+        Set the message for the confirmation widget.
+        """
+        if self._editing or self._making_new:
+            self._confirm_edit_delete.message = (
+                f"Replace value of this {self._display_name}?"
+            )
+        elif self._deleting:
+            self._confirm_edit_delete.message = f"Delete this {self._display_name}?"
+
     def _make_new_widget(self):
         """
         Make a new widget for the item type and set up actions for the save button.
@@ -282,10 +330,14 @@ class ChooseOrMakeNew(ipw.VBox):
             try:
                 self._saved_settings.add_item(new_widget.model(**new_widget.value))
             except ValueError:
-                # This will happen if the item already exists
-                self._confirm_edit.show()
+                # This will happen in two circumstances if the item already exists:
+                # 1. User is editing an existing item
+                # 2. User is making a new item with the same name as an existing one
+                self._set_confirm_message()
+                self._confirm_edit_delete.show()
             else:
                 # If saving works, we update the choices and select the new item
+                self._making_new = False
                 update_choices_and_select_new()
 
         def update_choices_and_select_new():
@@ -293,12 +345,12 @@ class ChooseOrMakeNew(ipw.VBox):
             Update the choices after a new item is saved, update the choices
             and select the new item.
             """
-            if not self._editing:
+            if not (self._editing or self._making_new):
                 value_to_select = new_widget.model(**new_widget.value)
                 self._construct_choices()
                 self._choose_existing.value = value_to_select
                 # Make sure the edit button is displayed
-                self._edit_button.layout.display = "flex"
+                self._edit_delete_container.layout.display = "flex"
 
         def revert_to_saved_value():
             """
@@ -328,16 +380,18 @@ class ChooseOrMakeNew(ipw.VBox):
         """
 
         # Use a closure here to capture the current state of the widget
-        def save_confirm_handler(change):
+        def confirm_handler(change):
             """
             This handles interactions with the confirmation widget, which is displayed
-            when the user either tries to save a new item with the same name as an
-            existing one or tries to save an edited item with the same name as an
-            existing one.
+            when the user has done any of these things:
+
+            + tried to save a new item with the same name as an existing one
+            + tried to save an existing item they have edited
+            + tried to delete an existing item.
 
             The widget has three possible values: True (yes), False (no), and None
 
-            This widget is called whent the widget value changes, which can happen two
+            This widget is called when the widget value changes, which can happen two
             ways:
 
             1. The user clicks the "yes" or "no" button, in which case the value will
@@ -350,28 +404,56 @@ class ChooseOrMakeNew(ipw.VBox):
             # value of None means the widget has been reset to not answered
             if change["new"] is not None:
                 item = self._item_widget.model(**self._item_widget.value)
-                if change["new"]:
-                    # Use has said yes to updating the item, which we do by
-                    # deleting the old one and adding the new one.
-                    self._saved_settings.delete_item(item, confirm=True)
-                    self._saved_settings.add_item(item)
-                    # Rebuild the dropdown list
-                    self._construct_choices()
-                    # Select the edited item
-                    self._choose_existing.value = item
-                else:
-                    # Use has said no to updating the item, so we just
-                    # act as though the user has selected this item.
-                    self._handle_selection({"new": item})
+                if self._editing or self._making_new:
+                    if change["new"]:
+                        # User has said yes to updating the item, which we do by
+                        # deleting the old one and adding the new one.
+                        self._saved_settings.delete_item(item, confirm=True)
+                        self._saved_settings.add_item(item)
+                        # Rebuild the dropdown list
+                        self._construct_choices()
+                        # Select the edited item
+                        self._choose_existing.value = item
+                    else:
+                        # User has said no to updating the item, so we just
+                        # act as though the user has selected this item.
+                        if self._editing:
+                            self._handle_selection({"new": item})
+                        else:
+                            # Set the selection to the first choice if there is one
+                            self._choose_existing.value = self._choose_existing.options[
+                                0
+                            ][1]
+                    # We are done editing/making new regardless of
+                    # the confirmation outcome
+                    self._editing = False
+                    self._making_new = False
+
+                elif self._deleting:
+                    if change["new"]:
+                        # User has confirmed the deletion
+                        self._saved_settings.delete_item(item, confirm=True)
+                        # Rebuild the dropdown list
+                        self._construct_choices()
+
+                        # Select the first item...
+                        self._choose_existing.value = self._choose_existing.options[0][
+                            1
+                        ]
+                        # ...but if there is only one option, the line above doesn't
+                        # trigger the _choose_existing observer because the value is set
+                        # when the options are set. So we need to trigger it manually.
+                        if len(self._choose_existing.options) == 1:
+                            self._handle_selection({"new": self._choose_existing.value})
+                    else:
+                        # User has decided not to delete the item
+                        self._handle_selection({"new": item})
+                    self._deleting = False
 
                 # Reset the confirmation widget to unanswered
-                self._confirm_edit.value = None
-            # We are done editing regardless of the confirmation outcome
-            self._editing = False
-            # Bring the edit button back
-            self._edit_button.layout.display = "flex"
+                self._confirm_edit_delete.value = None
 
-        return save_confirm_handler
+        return confirm_handler
 
     def _get_item(self, item_name):
         """


Test patch:
diff --git a/stellarphot/settings/tests/test_custom_widgets.py b/stellarphot/settings/tests/test_custom_widgets.py
index d26f767..072d083 100644
--- a/stellarphot/settings/tests/test_custom_widgets.py
+++ b/stellarphot/settings/tests/test_custom_widgets.py
@@ -16,6 +16,15 @@ class TestChooseOrMakeNew:
     Class for testing the ChooseOrMakeNew widget.
     """
 
+    def make_test_camera(self, path):
+        """
+        Make a camera with the default testing values and save it. This came
+        up often enough to warrant its own method.
+        """
+        saved = SavedSettings(_testing_path=path)
+        camera = Camera(**TEST_CAMERA_VALUES)
+        saved.add_item(camera)
+
     def test_creation_without_type_raises_error(self):
         # Should raise an error if no type is provided
         with pytest.raises(TypeError):
@@ -65,6 +74,9 @@ class TestChooseOrMakeNew:
         # No need for the confirmation dialog because we are not overwriting
         # anything.
 
+        # making sure the widget state is properly updated
+        assert choose_or_make_new._making_new is False
+
         # Check what we created using SavedSettings...
         saved = SavedSettings(_testing_path=tmp_path)
         cameras = saved.get_items("camera")
@@ -101,20 +113,18 @@ class TestChooseOrMakeNew:
 
     def test_edit_requires_confirmation(self, tmp_path):
         # Should require confirmation if the item already exists
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # the edit button should be displayed and the confirm widget should be hidden
         # note: display typically start as None or an empty string, so we just check
         # that it is not "none", which is what it will be set to when it is hidden.
-        assert choose_or_make_new._edit_button.layout.display != "none"
-        assert choose_or_make_new._confirm_edit.layout.display == "none"
+        assert choose_or_make_new._edit_delete_container.layout.display != "none"
+        assert choose_or_make_new._confirm_edit_delete.layout.display == "none"
 
         # Simulate a click on the edit button...
         choose_or_make_new._edit_button.click()
         # The edit button should now be hidden
-        assert choose_or_make_new._edit_button.layout.display == "none"
+        assert choose_or_make_new._edit_delete_container.layout.display == "none"
 
         # The savebuttonbar should be displayed
         assert choose_or_make_new._item_widget.savebuttonbar.layout.display != "none"
@@ -123,13 +133,15 @@ class TestChooseOrMakeNew:
         choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
 
         # The confirm dialog should be displayed
-        assert choose_or_make_new._confirm_edit.layout.display != "none"
+        assert choose_or_make_new._confirm_edit_delete.layout.display != "none"
+
+        # The confirmation dialog should contain the word "replace"
+        assert "replace" in choose_or_make_new._confirm_edit_delete.message.lower()
 
     def test_edit_item_saved_after_confirm(self, tmp_path):
         # Should save the item after confirmation
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
+
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # Simulate a click on the edit button...
         choose_or_make_new._edit_button.click()
@@ -140,16 +152,16 @@ class TestChooseOrMakeNew:
         # Simulate a click on the save button...
         choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
         # Simulate a click on the confirm button...
-        choose_or_make_new._confirm_edit._yes.click()
+        choose_or_make_new._confirm_edit_delete._yes.click()
 
+        saved = SavedSettings(_testing_path=tmp_path)
         cameras = saved.get_items("camera")
-        assert cameras.as_dict[camera.name].gain == new_gain
+        assert cameras.as_dict[TEST_CAMERA_VALUES["name"]].gain == new_gain
 
     def test_edit_item_not_saved_after_cancel(self, tmp_path):
         # Should not save the item after clicking the No button
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
+
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # Simulate a click on the edit button...
         choose_or_make_new._edit_button.click()
@@ -160,25 +172,27 @@ class TestChooseOrMakeNew:
         # Simulate a click on the save button...
         choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
         # Simulate a click on the cancel button...
-        choose_or_make_new._confirm_edit._no.click()
+        choose_or_make_new._confirm_edit_delete._no.click()
 
+        saved = SavedSettings(_testing_path=tmp_path)
         cameras = saved.get_items("camera")
-        assert cameras.as_dict[camera.name].gain == TEST_CAMERA_VALUES["gain"]
+        assert (
+            cameras.as_dict[TEST_CAMERA_VALUES["name"]].gain
+            == TEST_CAMERA_VALUES["gain"]
+        )
 
     def test_selecting_make_new_as_selection_works(self, tmp_path):
         # Should allow the user to select "Make new" as a selection
 
         # Make a camera so that we can test that selecting "Make new" works
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
 
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # This is the value for the "Make new...." option
         choose_or_make_new._choose_existing.value = "none"
 
         # edit button should go away
-        assert choose_or_make_new._edit_button.layout.display == "none"
+        assert choose_or_make_new._edit_delete_container.layout.display == "none"
 
         # Widget should be enabled
         assert choose_or_make_new._item_widget.disabled is False
@@ -206,8 +220,6 @@ class TestChooseOrMakeNew:
 
         # Make sure the correct first observatory is selected
         assert choose_or_make_new._choose_existing.value == observatory
-        print(f"{choose_or_make_new._item_widget.value=}")
-        print(f"{observatory.model_dump()=}")
         assert Observatory(**choose_or_make_new._item_widget.value) == observatory
 
         # Select the other observatory
@@ -264,7 +276,7 @@ class TestChooseOrMakeNew:
         assert len(saved.get_items("camera").as_dict) == 0
 
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
-        assert choose_or_make_new._edit_button.layout.display == "none"
+        assert choose_or_make_new._edit_delete_container.layout.display == "none"
 
     def test_edit_button_returns_after_making_new_item(self, tmp_path):
         # After making a new item the edit button should be displayed
@@ -273,7 +285,7 @@ class TestChooseOrMakeNew:
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
 
         # Make sure the edit button is hidden
-        assert choose_or_make_new._edit_button.layout.display == "none"
+        assert choose_or_make_new._edit_delete_container.layout.display == "none"
 
         # Change a value in the camera so we can check that the new value is saved.
         choose_or_make_new._item_widget.value = TEST_CAMERA_VALUES
@@ -281,19 +293,19 @@ class TestChooseOrMakeNew:
         choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
 
         # The edit button should now be displayed
-        assert choose_or_make_new._edit_button.layout.display != "none"
+        assert choose_or_make_new._edit_delete_container.layout.display != "none"
 
         # Select Make a new camera again
         choose_or_make_new._choose_existing.value = "none"
 
         # The edit button should now be hidden
-        assert choose_or_make_new._edit_button.layout.display == "none"
+        assert choose_or_make_new._edit_delete_container.layout.display == "none"
 
         # Select the camera we made
         choose_or_make_new._choose_existing.value = Camera(**TEST_CAMERA_VALUES)
 
         # The edit button should now be displayed
-        assert choose_or_make_new._edit_button.layout.display != "none"
+        assert choose_or_make_new._edit_delete_container.layout.display != "none"
 
     def test_save_button_disabled_when_no_changes(self, tmp_path):
         # Immediately after the edit button has been clicked, the save button should be
@@ -301,9 +313,7 @@ class TestChooseOrMakeNew:
         #
         # That should remain true even after the user makes a change, then saves it
         # and then edits again but has not yet made any changes.
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
 
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # Simulate a click on the edit button...
@@ -323,7 +333,7 @@ class TestChooseOrMakeNew:
         choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
 
         # Confirm the save
-        choose_or_make_new._confirm_edit._yes.click()
+        choose_or_make_new._confirm_edit_delete._yes.click()
 
         # Edit again...
         choose_or_make_new._edit_button.click()
@@ -334,9 +344,7 @@ class TestChooseOrMakeNew:
     def test_revert_button_is_enabled_after_clicking_edit(self, tmp_path):
         # The revert button should be enabled after clicking the edit button so
         # the user can cancel the edit.
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
 
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # Simulate a click on the edit button...
@@ -347,9 +355,7 @@ class TestChooseOrMakeNew:
 
     def test_clicking_revert_button_cancels_edit(self, tmp_path):
         # Clicking the revert button should cancel the edit
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
 
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # Simulate a click on the edit button...
@@ -362,7 +368,7 @@ class TestChooseOrMakeNew:
         assert choose_or_make_new._item_widget.value == TEST_CAMERA_VALUES
 
         # The edit button should be displayed
-        assert choose_or_make_new._edit_button.layout.display != "none"
+        assert choose_or_make_new._edit_delete_container.layout.display != "none"
 
         # We should not be in editing mode anymore
         assert not choose_or_make_new._editing
@@ -372,9 +378,7 @@ class TestChooseOrMakeNew:
     ):
         # The revert button should remain enabled if the value is invalid and reverting
         # should actually revert the value.
-        saved = SavedSettings(_testing_path=tmp_path)
-        camera = Camera(**TEST_CAMERA_VALUES)
-        saved.add_item(camera)
+        self.make_test_camera(tmp_path)
 
         choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
         # Simulate a click on the edit button...
@@ -401,6 +405,151 @@ class TestChooseOrMakeNew:
         # The camera should not have been changed
         assert choose_or_make_new._item_widget.value == TEST_CAMERA_VALUES
 
+    def test_delete_button_click_displays_confirm_dialog(self, tmp_path):
+        # Clicking "Delete" should display a confirmation dialog
+
+        # Make an item to ensure an existing item is displayed when the widget is made
+        self.make_test_camera(tmp_path)
+
+        choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
+        choose_or_make_new._delete_button.click()
+
+        # Confirm dialog should be displayed
+        assert choose_or_make_new._confirm_edit_delete.layout.display != "none"
+        # edit/delete buttons should be hidden
+        assert choose_or_make_new._edit_delete_container.layout.display == "none"
+        # The confirm dialog should indicate you are deleting
+        assert "delete" in choose_or_make_new._confirm_edit_delete.message.lower()
+
+    @pytest.mark.parametrize("click_yes", [True, False])
+    def test_delete_actions_after_confirmation(self, tmp_path, click_yes):
+        # Clicking "Yes" in the confirmation dialog should delete the item
+        self.make_test_camera(tmp_path)
+
+        choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
+        choose_or_make_new._delete_button.click()
+
+        if click_yes:
+            # Simulate a click on the "Yes" button in the confirm dialog
+            choose_or_make_new._confirm_edit_delete._yes.click()
+
+            # The camera should be gone
+            saved = SavedSettings(_testing_path=tmp_path)
+            cameras = saved.get_items("camera")
+            assert len(cameras.as_dict) == 0
+        else:
+            # Simulate a click on the "No" button in the confirm dialog
+            choose_or_make_new._confirm_edit_delete._no.click()
+
+            # The camera should still be there
+            saved = SavedSettings(_testing_path=tmp_path)
+            cameras = saved.get_items("camera")
+            assert len(cameras.as_dict) == 1
+            assert list(cameras.as_dict.values())[0].model_dump() == TEST_CAMERA_VALUES
+
+        # Regardless of the choice, the confirm dialog should be hidden
+        assert choose_or_make_new._confirm_edit_delete.layout.display == "none"
+
+        # We should also no longer be deleting
+        assert not choose_or_make_new._deleting
+
+        # If the user clicked "No" the edit/delete buttons should be displayed because
+        # there is still an item to edit or delete.
+        if not click_yes:
+            assert choose_or_make_new._edit_delete_container.layout.display != "none"
+        # If the user clicked "Yes" the edit/delete buttons should be hidden because
+        # there is no item to edit or delete.
+        else:
+            assert choose_or_make_new._edit_delete_container.layout.display == "none"
+
+    def test_correct_item_selected_after_delete_and_yes(self, tmp_path):
+        # The correct item should be selected after an item is deleted and the user
+        # clicks "Yes" in the confirmation dialog.
+        # Make two cameras
+        self.make_test_camera(tmp_path)
+        saved = SavedSettings(_testing_path=tmp_path)
+        camera2 = Camera(**TEST_CAMERA_VALUES)
+        camera2.name = "zzzz" + camera2.name
+        saved.add_item(camera2)
+
+        choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
+        # Select the first camera...
+        choose_or_make_new._choose_existing.value = saved.cameras.as_dict[
+            TEST_CAMERA_VALUES["name"]
+        ]
+        # ...and delete it
+        choose_or_make_new._delete_button.click()
+        choose_or_make_new._confirm_edit_delete._yes.click()
+        # Are the edit/delete buttons being shown?
+        assert choose_or_make_new._edit_delete_container.layout.display != "none"
+        # Is the correct camera selected?
+        assert choose_or_make_new._choose_existing.value == camera2
+
+    @pytest.mark.parametrize("click_yes", [True, False])
+    def test_making_new_with_same_name_as_existing(self, tmp_path, click_yes):
+        # Making a new item with the same name as an existing item should require
+        # confirmation
+        self.make_test_camera(tmp_path)
+
+        choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
+
+        # Click the "Make new" button
+        choose_or_make_new._choose_existing.value = "none"
+
+        # set the item widget value to the existing camera with double the gain
+        camera_values = TEST_CAMERA_VALUES.copy()
+        camera_values["gain"] = 2 * camera_values["gain"]
+        choose_or_make_new._item_widget.value = Camera(**camera_values)
+
+        # Simulate a click on the save button...
+        choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
+
+        # The confirm dialog should be displayed
+        assert choose_or_make_new._confirm_edit_delete.layout.display != "none"
+        # The confirm dialog should contain the word "replace"
+        assert "replace" in choose_or_make_new._confirm_edit_delete.message.lower()
+
+        if click_yes:
+            # ...click confirm
+            choose_or_make_new._confirm_edit_delete._yes.click()
+            expected_camera = Camera(**camera_values)
+        else:
+            # ...click cancel
+            choose_or_make_new._confirm_edit_delete._no.click()
+            expected_camera = Camera(**TEST_CAMERA_VALUES)
+
+        # Check the value of the camera
+        chosen_cam = Camera(**choose_or_make_new._item_widget.value)
+        assert chosen_cam == choose_or_make_new._choose_existing.value
+
+        assert chosen_cam == expected_camera
+
+    def test_weird_sequence_of_no_clicks(self, tmp_path):
+        # This is a regression test for #320
+        # Make a camera
+        self.make_test_camera(tmp_path)
+
+        # Make a new camera...
+        choose_or_make_new = ChooseOrMakeNew("camera", _testing_path=tmp_path)
+        choose_or_make_new._choose_existing.value = "none"
+        choose_or_make_new._item_widget.value = TEST_CAMERA_VALUES
+
+        # ...click save...
+        choose_or_make_new._item_widget.savebuttonbar.bn_save.click()
+        # ...but say no to confirm
+        choose_or_make_new._confirm_edit_delete._no.click()
+
+        # Check that we are back to our selected camera
+        assert choose_or_make_new._choose_existing.value == Camera(**TEST_CAMERA_VALUES)
+
+        # Click delete...
+        choose_or_make_new._delete_button.click()
+        # ...but say no to confirm...
+        choose_or_make_new._confirm_edit_delete._no.click()
+
+        # ...the edit/delete buttons should be displayed
+        assert choose_or_make_new._edit_delete_container.layout.display != "none"
+
 
 class TestConfirm:
     def test_initial_value(self):