Repository: napari/napari

Issue:
Errors when trying to interact with an empty shapes layer from the layers context menu actions
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->
Trying to duplicate a shapes layer with the `Duplicate Layer` action from the layers context menu raises an exception. Selecting the also active option `Convert to Labels` raises an exception.

## To Reproduce

Steps to reproduce the behavior:

1. Launch Napari
2. Create a new shapes layer with the `New shapes layer` button
3. Right click the layer from the layers list
4. Select from the context menu the `Duplicate Layer` or `Convert to Labels` action

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

### Duplicate Layer traceback (`IndexError: list index out of range`)

<details>

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
File ~\anaconda3\envs\napari-dev\lib\site-packages\app_model\backends\qt\_qaction.py:52, in QCommandAction._on_triggered(self=QMenuItemAction(MenuItem(when=None, group='5_spl...tle=None, toggled=None), alt=None), app='napari'), checked=False)
     48 def _on_triggered(self, checked: bool) -> None:
     49     # execute_command returns a Future, for the sake of eventually being
     50     # asynchronous without breaking the API.  For now, we call result()
     51     # to raise any exceptions.
---> 52     self._app.commands.execute_command(self._command_id).result()
        self._command_id = 'napari:layer:duplicate'
        self = QMenuItemAction(MenuItem(when=None, group='5_split_merge', order=None, command=CommandRule(id='napari:layer:duplicate', title='Duplicate Layer', category=None, tooltip=None, status_tip=None, icon=None, enablement=None, short_title=None, toggled=None), alt=None), app='napari')
        self._app = Application('napari')

File ~\anaconda3\envs\napari-dev\lib\site-packages\app_model\registries\_commands_reg.py:202, in CommandsRegistry.execute_command(self=<CommandsRegistry at 0x1d0fd6a5b10 (86 commands)>, id='napari:layer:duplicate', execute_asychronously=False, *args=(), **kwargs={})
    198 except Exception as e:
    199     if self._raise_synchronous_exceptions:
    200         # note, the caller of this function can also achieve this by
    201         # calling `future.result()` on the returned future object.
--> 202         raise e
    203     future.set_exception(e)
    205 return future

File ~\anaconda3\envs\napari-dev\lib\site-packages\app_model\registries\_commands_reg.py:197, in CommandsRegistry.execute_command(self=<CommandsRegistry at 0x1d0fd6a5b10 (86 commands)>, id='napari:layer:duplicate', execute_asychronously=False, *args=(), **kwargs={})
    195 future: Future = Future()
    196 try:
--> 197     future.set_result(cmd(*args, **kwargs))
        future = <Future at 0x1d089c429e0 state=pending>
        cmd = <function _duplicate_layer at 0x000001D08B2E2A70>
        args = ()
        kwargs = {}
    198 except Exception as e:
    199     if self._raise_synchronous_exceptions:
    200         # note, the caller of this function can also achieve this by
    201         # calling `future.result()` on the returned future object.

File ~\anaconda3\envs\napari-dev\lib\site-packages\in_n_out\_store.py:890, in Store.inject_processors.<locals>._deco.<locals>._exec(*args=(), **kwargs={})
    888 @wraps(func)
    889 def _exec(*args: P.args, **kwargs: P.kwargs) -> R:
--> 890     result = func(*args, **kwargs)
        func = <function _duplicate_layer at 0x000001D08B2E2B00>
        args = ()
        kwargs = {}
    891     if result is not None:
    892         self.process(
    893             result,
    894             type_hint=type_hint,
    895             first_processor_only=first_processor_only,
    896             raise_exception=raise_exception,
    897         )

File ~\anaconda3\envs\napari-dev\lib\site-packages\in_n_out\_store.py:774, in Store.inject.<locals>._inner.<locals>._exec(*args=(), **kwargs={})
    772 # call the function with injected values
    773 try:
--> 774     result = func(**_kwargs)
        _kwargs = {'ll': [<Shapes layer 'Shapes' at 0x1d093d5fe50>], 'name': ''}
        func = <function _duplicate_layer at 0x000001D0FD1E9CF0>
    775 except TypeError as e:
    776     # likely a required argument is still missing.
    777     # show what was injected and raise
    778     _argnames = (
    779         f"arguments: {set(_kwargs)!r}" if _kwargs else "NO arguments"
    780     )

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\_layer_actions.py:29, in _duplicate_layer(ll=[<Shapes layer 'Shapes' at 0x1d093d5fe50>], name='')
     27 state["name"] = trans._('{name} copy', name=lay.name)
     28 with layer_source(parent=lay):
---> 29     new = Layer.create(deepcopy(data), state, type_str)
        data = []
        state = {'name': 'Shapes copy', 'metadata': {}, 'scale': [1.0, 1.0], 'translate': [0.0, 0.0], 'rotate': [[1.0, 0.0], [0.0, 1.0]], 'shear': [0.0], 'affine': <class 'numpy.ndarray'> (3, 3) float64, 'opacity': 0.7, 'blending': 'translucent', 'visible': True, 'experimental_clipping_planes': [], 'ndim': 2, 'properties': {}, 'property_choices': {}, 'text': {'string': {'constant': <class 'numpy.ndarray'> () <U1, 'encoding_type': 'ConstantStringEncoding'}, 'color': {'constant': <class 'numpy.ndarray'> (4,) float32, 'encoding_type': 'ConstantColorEncoding'}, 'visible': True, 'size': 12, 'blending': <Blending.TRANSLUCENT: 'translucent'>, 'anchor': <Anchor.CENTER: 'center'>, 'translation': <class 'numpy.ndarray'> () float64, 'rotation': 0.0}, 'shape_type': [], 'z_index': [], 'edge_width': [], 'face_color': <class 'numpy.ndarray'> (0, 4) float64, 'face_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'face_colormap': 'viridis', 'face_contrast_limits': None, 'edge_color': <class 'numpy.ndarray'> (0, 4) float64, 'edge_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'edge_colormap': 'viridis', 'edge_contrast_limits': None, 'features': Empty DataFrame
Columns: []
Index: []}
        type_str = 'shapes'
        Layer = <class 'napari.layers.base.base.Layer'>
     30 ll.insert(ll.index(lay) + 1, new)

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\base\base.py:1861, in Layer.create(cls=<class 'napari.layers.base.base.Layer'>, data=[], meta={'affine': <class 'numpy.ndarray'> (3, 3) float64, 'blending': 'translucent', 'edge_color': <class 'numpy.ndarray'> (0, 4) float64, 'edge_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'edge_colormap': 'viridis', 'edge_contrast_limits': None, 'edge_width': [], 'experimental_clipping_planes': [], 'face_color': <class 'numpy.ndarray'> (0, 4) float64, 'face_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, ...}, layer_type='shapes')
   1859 except Exception as exc:  # noqa: BLE001
   1860     if 'unexpected keyword argument' not in str(exc):
-> 1861         raise exc
   1863     bad_key = str(exc).split('keyword argument ')[-1]
   1864     raise TypeError(
   1865         trans._(
   1866             "_add_layer_from_data received an unexpected keyword argument ({bad_key}) for layer type {layer_type}",
   (...)
   1870         )
   1871     ) from exc

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\base\base.py:1858, in Layer.create(cls=<class 'napari.layers.base.base.Layer'>, data=[], meta={'affine': <class 'numpy.ndarray'> (3, 3) float64, 'blending': 'translucent', 'edge_color': <class 'numpy.ndarray'> (0, 4) float64, 'edge_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'edge_colormap': 'viridis', 'edge_contrast_limits': None, 'edge_width': [], 'experimental_clipping_planes': [], 'face_color': <class 'numpy.ndarray'> (0, 4) float64, 'face_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, ...}, layer_type='shapes')
   1855 Cls = getattr(layers, layer_type.title())
   1857 try:
-> 1858     return Cls(data, **(meta or {}))
        Cls = <class 'napari.layers.shapes.shapes.Shapes'>
        data = []
        meta or {} = {'name': 'Shapes copy', 'metadata': {}, 'scale': [1.0, 1.0], 'translate': [0.0, 0.0], 'rotate': [[1.0, 0.0], [0.0, 1.0]], 'shear': [0.0], 'affine': <class 'numpy.ndarray'> (3, 3) float64, 'opacity': 0.7, 'blending': 'translucent', 'visible': True, 'experimental_clipping_planes': [], 'ndim': 2, 'properties': {}, 'property_choices': {}, 'text': {'string': {'constant': <class 'numpy.ndarray'> () <U1, 'encoding_type': 'ConstantStringEncoding'}, 'color': {'constant': <class 'numpy.ndarray'> (4,) float32, 'encoding_type': 'ConstantColorEncoding'}, 'visible': True, 'size': 12, 'blending': <Blending.TRANSLUCENT: 'translucent'>, 'anchor': <Anchor.CENTER: 'center'>, 'translation': <class 'numpy.ndarray'> () float64, 'rotation': 0.0}, 'shape_type': [], 'z_index': [], 'edge_width': [], 'face_color': <class 'numpy.ndarray'> (0, 4) float64, 'face_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'face_colormap': 'viridis', 'face_contrast_limits': None, 'edge_color': <class 'numpy.ndarray'> (0, 4) float64, 'edge_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'edge_colormap': 'viridis', 'edge_contrast_limits': None, 'features': Empty DataFrame
Columns: []
Index: []}
        meta = {'name': 'Shapes copy', 'metadata': {}, 'scale': [1.0, 1.0], 'translate': [0.0, 0.0], 'rotate': [[1.0, 0.0], [0.0, 1.0]], 'shear': [0.0], 'affine': <class 'numpy.ndarray'> (3, 3) float64, 'opacity': 0.7, 'blending': 'translucent', 'visible': True, 'experimental_clipping_planes': [], 'ndim': 2, 'properties': {}, 'property_choices': {}, 'text': {'string': {'constant': <class 'numpy.ndarray'> () <U1, 'encoding_type': 'ConstantStringEncoding'}, 'color': {'constant': <class 'numpy.ndarray'> (4,) float32, 'encoding_type': 'ConstantColorEncoding'}, 'visible': True, 'size': 12, 'blending': <Blending.TRANSLUCENT: 'translucent'>, 'anchor': <Anchor.CENTER: 'center'>, 'translation': <class 'numpy.ndarray'> () float64, 'rotation': 0.0}, 'shape_type': [], 'z_index': [], 'edge_width': [], 'face_color': <class 'numpy.ndarray'> (0, 4) float64, 'face_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'face_colormap': 'viridis', 'face_contrast_limits': None, 'edge_color': <class 'numpy.ndarray'> (0, 4) float64, 'edge_color_cycle': <class 'numpy.ndarray'> (2, 4) float32, 'edge_colormap': 'viridis', 'edge_contrast_limits': None, 'features': Empty DataFrame
Columns: []
Index: []}
   1859 except Exception as exc:  # noqa: BLE001
   1860     if 'unexpected keyword argument' not in str(exc):

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py:424, in Shapes.__init__(self=<class 'napari.layers.shapes.shapes.Shapes'> instance, data=[], ndim=2, features=Empty DataFrame
Columns: []
Index: [], properties={}, property_choices={}, text={'anchor': <Anchor.CENTER: 'center'>, 'blending': <Blending.TRANSLUCENT: 'translucent'>, 'color': {'constant': <class 'numpy.ndarray'> (4,) float32, 'encoding_type': 'ConstantColorEncoding'}, 'rotation': 0.0, 'size': 12, 'string': {'constant': <class 'numpy.ndarray'> () <U1, 'encoding_type': 'ConstantStringEncoding'}, 'translation': <class 'numpy.ndarray'> () float64, 'visible': True}, shape_type=[], edge_width=[], edge_color=<class 'numpy.ndarray'> (0, 4) float64, edge_color_cycle=<class 'numpy.ndarray'> (2, 4) float32, edge_colormap='viridis', edge_contrast_limits=None, face_color=<class 'numpy.ndarray'> (0, 4) float64, face_color_cycle=<class 'numpy.ndarray'> (2, 4) float32, face_colormap='viridis', face_contrast_limits=None, z_index=[], name='Shapes copy', metadata={}, scale=[1.0, 1.0], translate=[0.0, 0.0], rotate=[[1.0, 0.0], [0.0, 1.0]], shear=[0.0], affine=<class 'numpy.ndarray'> (3, 3) float64, opacity=0.7, blending='translucent', visible=True, cache=True, experimental_clipping_planes=[])
    422 else:
    423     data, shape_type = extract_shape_type(data, shape_type)
--> 424     data_ndim = get_shape_ndim(data)
        data = []
    425     if ndim is not None and ndim != data_ndim:
    426         raise ValueError(
    427             trans._(
    428                 "Shape dimensions must be equal to ndim",
    429                 deferred=True,
    430             )
    431         )

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shapes_utils.py:1062, in get_shape_ndim(data=[])
   1060     ndim = np.array(data).shape[2]
   1061 # just one shape
-> 1062 elif np.array(data[0]).ndim == 1:
        data = []
        np = <module 'numpy' from 'C:\\Users\\dalth\\anaconda3\\envs\\napari-dev\\lib\\site-packages\\numpy\\__init__.py'>
   1063     ndim = np.array(data).shape[1]
   1064 # list of different shapes
   1065 else:

IndexError: list index out of range
```

</details>

### Convert to Labels traceback (`ValueError: negative dimensions are not allowed`)

<details>

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~\anaconda3\envs\napari-dev\lib\site-packages\app_model\backends\qt\_qaction.py:52, in QCommandAction._on_triggered(self=QMenuItemAction(MenuItem(when=None, group='1_con...tle=None, toggled=None), alt=None), app='napari'), checked=False)
     48 def _on_triggered(self, checked: bool) -> None:
     49     # execute_command returns a Future, for the sake of eventually being
     50     # asynchronous without breaking the API.  For now, we call result()
     51     # to raise any exceptions.
---> 52     self._app.commands.execute_command(self._command_id).result()
        self._command_id = 'napari:layer:convert_to_labels'
        self = QMenuItemAction(MenuItem(when=None, group='1_conversion', order=None, command=CommandRule(id='napari:layer:convert_to_labels', title='Convert to Labels', category=None, tooltip=None, status_tip=None, icon=None, enablement=Expr.parse('num_selected_image_layers >= 1 or num_selected_shapes_layers >= 1 and all_selected_layers_same_type'), short_title=None, toggled=None), alt=None), app='napari')
        self._app = Application('napari')

File ~\anaconda3\envs\napari-dev\lib\site-packages\app_model\registries\_commands_reg.py:202, in CommandsRegistry.execute_command(self=<CommandsRegistry at 0x1d0fd6a5b10 (86 commands)>, id='napari:layer:convert_to_labels', execute_asychronously=False, *args=(), **kwargs={})
    198 except Exception as e:
    199     if self._raise_synchronous_exceptions:
    200         # note, the caller of this function can also achieve this by
    201         # calling `future.result()` on the returned future object.
--> 202         raise e
    203     future.set_exception(e)
    205 return future

File ~\anaconda3\envs\napari-dev\lib\site-packages\app_model\registries\_commands_reg.py:197, in CommandsRegistry.execute_command(self=<CommandsRegistry at 0x1d0fd6a5b10 (86 commands)>, id='napari:layer:convert_to_labels', execute_asychronously=False, *args=(), **kwargs={})
    195 future: Future = Future()
    196 try:
--> 197     future.set_result(cmd(*args, **kwargs))
        future = <Future at 0x1d090af6cb0 state=pending>
        cmd = <function _convert_to_labels at 0x000001D08B2132E0>
        args = ()
        kwargs = {}
    198 except Exception as e:
    199     if self._raise_synchronous_exceptions:
    200         # note, the caller of this function can also achieve this by
    201         # calling `future.result()` on the returned future object.

File ~\anaconda3\envs\napari-dev\lib\site-packages\in_n_out\_store.py:890, in Store.inject_processors.<locals>._deco.<locals>._exec(*args=(), **kwargs={})
    888 @wraps(func)
    889 def _exec(*args: P.args, **kwargs: P.kwargs) -> R:
--> 890     result = func(*args, **kwargs)
        func = <function _convert_to_labels at 0x000001D092F0F130>
        args = ()
        kwargs = {}
    891     if result is not None:
    892         self.process(
    893             result,
    894             type_hint=type_hint,
    895             first_processor_only=first_processor_only,
    896             raise_exception=raise_exception,
    897         )

File ~\anaconda3\envs\napari-dev\lib\site-packages\in_n_out\_store.py:774, in Store.inject.<locals>._inner.<locals>._exec(*args=(), **kwargs={})
    772 # call the function with injected values
    773 try:
--> 774     result = func(**_kwargs)
        _kwargs = {'ll': []}
        func = <function _convert_to_labels at 0x000001D0FD210700>
    775 except TypeError as e:
    776     # likely a required argument is still missing.
    777     # show what was injected and raise
    778     _argnames = (
    779         f"arguments: {set(_kwargs)!r}" if _kwargs else "NO arguments"
    780     )

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\_layer_actions.py:70, in _convert_to_labels(ll=[])
     69 def _convert_to_labels(ll: LayerList):
---> 70     return _convert(ll, 'labels')
        ll = []

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\_layer_actions.py:57, in _convert(ll=[], type_='labels')
     55 ll.pop(idx)
     56 if isinstance(lay, Shapes) and type_ == 'labels':
---> 57     data = lay.to_labels()
        lay = <Shapes layer 'Shapes' at 0x1d093d5fe50>
     58 else:
     59     data = lay.data.astype(int) if type_ == 'labels' else lay.data

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py:2939, in Shapes.to_labels(self=<Shapes layer 'Shapes'>, labels_shape=<class 'numpy.ndarray'> (2,) int32)
   2936     labels_shape = np.round(self._extent_data[1]) + 1
   2938 labels_shape = np.ceil(labels_shape).astype('int')
-> 2939 labels = self._data_view.to_labels(labels_shape=labels_shape)
        labels_shape = <class 'numpy.ndarray'> (2,) int32
        self._data_view = <napari.layers.shapes._shape_list.ShapeList object at 0x000001D090EAE050>
        self = <Shapes layer 'Shapes' at 0x1d093d5fe50>
   2941 return labels

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shape_list.py:1188, in ShapeList.to_labels(self=<napari.layers.shapes._shape_list.ShapeList object>, labels_shape=<class 'numpy.ndarray'> (2,) int32, zoom_factor=1, offset=(0, 0))
   1185 if labels_shape is None:
   1186     labels_shape = self.displayed_vertices.max(axis=0).astype(np.int)
-> 1188 labels = np.zeros(labels_shape, dtype=int)
        labels_shape = <class 'numpy.ndarray'> (2,) int32
        np = <module 'numpy' from 'C:\\Users\\dalth\\anaconda3\\envs\\napari-dev\\lib\\site-packages\\numpy\\__init__.py'>
   1190 for ind in self._z_order[::-1]:
   1191     mask = self.shapes[ind].to_mask(
   1192         labels_shape, zoom_factor=zoom_factor, offset=offset
   1193     )

ValueError: negative dimensions are not allowed
```

</details>

## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

* Duplicate empty layer, so a new empty shapes layer with name `Shapes copy` is created or disable the `Duplicated Layer` action if the shapes layer is empty 
* Disable the `Convert to Labels` action if the shapes layer is empty.

## Environment

napari: 0.5.0a2.dev3+gea812316
Platform: Windows-10-10.0.19045-SP0
Python: 3.10.9 | packaged by conda-forge | (main, Jan 11 2023, 15:15:40) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.7
NumPy: 1.23.4
SciPy: 1.9.3
Dask: 2022.10.2
VisPy: 0.12.1
magicgui: 0.6.0
superqt: unknown
in-n-out: 0.1.5
app-model: 0.1.0
npe2: 0.6.1

OpenGL:
- GL version: 4.5.0 - Build 25.20.100.6444
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1366x768, scale 1.0
- screen 2: resolution 1920x1080, scale 1.0

Settings path:
- C:\Users\dalth\AppData\Local\napari\napari-dev_4454fdbf170edefe23827a4bf512389a6806af69\settings.yaml




Discussion:

Resolving patch:
diff --git a/napari/_app_model/actions/_layer_actions.py b/napari/_app_model/actions/_layer_actions.py
--- a/napari/_app_model/actions/_layer_actions.py
+++ b/napari/_app_model/actions/_layer_actions.py
@@ -70,6 +70,7 @@
                 | (LLCK.num_selected_shapes_layers >= 1)
             )
             & LLCK.all_selected_layers_same_type
+            & ~LLCK.selected_empty_shapes_layer
         ),
         menus=[LAYERCTX_CONVERSION],
     ),
diff --git a/napari/_app_model/context/_layerlist_context.py b/napari/_app_model/context/_layerlist_context.py
--- a/napari/_app_model/context/_layerlist_context.py
+++ b/napari/_app_model/context/_layerlist_context.py
@@ -130,6 +130,10 @@ def _active_is_image_3d(s: LayerSel) -> bool:
     )
 
 
+def _empty_shapes_layer_selected(s: LayerSel) -> bool:
+    return any(x._type_string == "shapes" and not len(x.data) for x in s)
+
+
 class LayerListContextKeys(ContextNamespace['LayerSel']):
     """These are the available context keys relating to a LayerList.
 
@@ -239,3 +243,8 @@ class LayerListContextKeys(ContextNamespace['LayerSel']):
         trans._("True when all selected layers are labels."),
         _only_labels,
     )
+    selected_empty_shapes_layer = ContextKey(
+        False,
+        trans._("True when there is a shapes layer without data selected."),
+        _empty_shapes_layer_selected,
+    )
diff --git a/napari/layers/shapes/shapes.py b/napari/layers/shapes/shapes.py
--- a/napari/layers/shapes/shapes.py
+++ b/napari/layers/shapes/shapes.py
@@ -418,7 +418,7 @@ def __init__(
         cache=True,
         experimental_clipping_planes=None,
     ) -> None:
-        if data is None:
+        if data is None or len(data) == 0:
             if ndim is None:
                 ndim = 2
             data = np.empty((0, 0, ndim))
@@ -1497,6 +1497,12 @@ def _get_state(self):
             Dictionary of layer state.
         """
         state = self._get_base_state()
+        face_color = self.face_color
+        edge_color = self.edge_color
+        if not face_color.size:
+            face_color = self._current_face_color
+        if not edge_color.size:
+            edge_color = self._current_edge_color
         state.update(
             {
                 'ndim': self.ndim,
@@ -1507,11 +1513,11 @@ def _get_state(self):
                 'opacity': self.opacity,
                 'z_index': self.z_index,
                 'edge_width': self.edge_width,
-                'face_color': self.face_color,
+                'face_color': face_color,
                 'face_color_cycle': self.face_color_cycle,
                 'face_colormap': self.face_colormap.name,
                 'face_contrast_limits': self.face_contrast_limits,
-                'edge_color': self.edge_color,
+                'edge_color': edge_color,
                 'edge_color_cycle': self.edge_color_cycle,
                 'edge_colormap': self.edge_colormap.name,
                 'edge_contrast_limits': self.edge_contrast_limits,


Test patch:
diff --git a/napari/layers/_tests/test_layer_actions.py b/napari/layers/_tests/test_layer_actions.py
--- a/napari/layers/_tests/test_layer_actions.py
+++ b/napari/layers/_tests/test_layer_actions.py
@@ -52,12 +52,13 @@ def test_toggle_visibility_with_linked_layers():
     assert layer_list[3].visible is True
 
 
-def test_duplicate_layers():
+@pytest.mark.parametrize('layer_type', [Points, Shapes])
+def test_duplicate_layers(layer_type):
     def _dummy():
         pass
 
     layer_list = LayerList()
-    layer_list.append(Points([[0, 0]], name="test"))
+    layer_list.append(layer_type([], name="test"))
     layer_list.selection.active = layer_list[0]
     layer_list[0].events.data.connect(_dummy)
     assert len(layer_list[0].events.data.callbacks) == 2