Repository: jeertmans/manim-slides (language: python)

Issue:
[BUG] 'OpenGLCamera' object has no attribute 'background_color' when rendering using manim.opengl
### Description

Hi, there! I have found that the newest V5.1.7 manim-slides can't render manim.opengl scene. The mwe is shown here:
```py
from manim import *
from manim.opengl import *
from manim_slides import Slide, ThreeDSlide
config.renderer = "opengl"

class Test(ThreeDSlide):
    def construct(self):
        cir = Circle()
        self.play(Create(cir))
        self.wait()
```
The terminal then outputs:
```bash
[05/17/24 19:26:03] INFO     Animation 0 : Partial movie file written in                             scene_file_writer.py:527
                             '/home/semikernel/Tiktok/Projects/OneMonthPlan/media/videos/gaza/1920p6                         
                             0/partial_movie_files/Test/2702378517_2531373006_223132457.mp4'                                 
[05/17/24 19:26:04] INFO     Animation 1 : Partial movie file written in                             scene_file_writer.py:527
                             '/home/semikernel/Tiktok/Projects/OneMonthPlan/media/videos/gaza/1920p6                         
                             0/partial_movie_files/Test/1069205256_3146877161_1023450693.mp4'                                
                    INFO     Combining to Movie file.                                                scene_file_writer.py:617
                    INFO                                                                             scene_file_writer.py:735
                             File ready at                                                                                   
                             '/home/semikernel/Tiktok/Projects/OneMonthPlan/media/videos/gaza/1920p6                         
                             0/Test.mp4'                                                                                     
                                                                                                                             
                    INFO     Rendered Test                                                                       scene.py:241
                             Played 2 animations                                                                             
[05/17/24 19:26:05] INFO     Generated 1 slides to                                                                base.py:503
                             '/home/semikernel/Tiktok/Projects/OneMonthPlan/slides/files/Test'                               
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /home/semikernel/.local/lib/python3.10/site-packages/manim/cli/render/commands.py:115 in render  │
│                                                                                                  │
│   112 │   │   │   try:                                                                           │
│   113 │   │   │   │   with tempconfig({}):                                                       │
│   114 │   │   │   │   │   scene = SceneClass()                                                   │
│ ❱ 115 │   │   │   │   │   scene.render()                                                         │
│   116 │   │   │   except Exception:                                                              │
│   117 │   │   │   │   error_console.print_exception()                                            │
│   118 │   │   │   │   sys.exit(1)                                                                │
│                                                                                                  │
│ /home/semikernel/.local/lib/python3.10/site-packages/manim_slides/slide/manim.py:97 in render    │
│                                                                                                  │
│    94 │   │                                                                                      │
│    95 │   │   config["max_files_cached"] = max_files_cached                                      │
│    96 │   │                                                                                      │
│ ❱  97 │   │   self._save_slides()                                                                │
│    98                                                                                            │
│    99                                                                                            │
│   100 class ThreeDSlide(Slide, ThreeDScene):  # type: ignore[misc]                               │
│                                                                                                  │
│ /home/semikernel/.local/lib/python3.10/site-packages/manim_slides/slide/base.py:512 in           │
│ _save_slides                                                                                     │
│                                                                                                  │
│   509 │   │   PresentationConfig(                                                                │
│   510 │   │   │   slides=slides,                                                                 │
│   511 │   │   │   resolution=self._resolution,                                                   │
│ ❱ 512 │   │   │   background_color=self._background_color,                                       │
│   513 │   │   ).to_file(slide_path)                                                              │
│   514 │   │                                                                                      │
│   515 │   │   logger.info(                                                                       │
│                                                                                                  │
│ /home/semikernel/.local/lib/python3.10/site-packages/manim_slides/slide/manim.py:26 in           │
│ _background_color                                                                                │
│                                                                                                  │
│    23 │                                                                                          │
│    24 │   @property                                                                              │
│    25 │   def _background_color(self) -> str:                                                    │
│ ❱  26 │   │   color = self.camera.background_color                                               │
│    27 │   │   if hex_color := getattr(color, "hex", None):                                       │
│    28 │   │   │   return hex_color  # type: ignore                                               │
│    29 │   │   else:  # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020        │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
AttributeError: 'OpenGLCamera' object has no attribute 'background_color'
```

### Version

manim-slides, version 5.1.7

### Platform

ubuntu 22.04

### Screenshots

_No response_

### Additional information
Actually in previous manim-slides, everything works well. I am wondering where the error comes from. Thx a lot:)
_No response_


Discussion / hints:
Hello, nice catch @semikernel!

I actually never use the OpenGL renderer, but I guess supporting it would be very easy.

Reading the `OpenGLCamera` class, it looks like I might need to try from `config["background_color"]` instead. Something similar might also be needed for the other attributes.

I don't have might time for this at the moment, but feel free to give it a try if you feel confident enough :-) 
> Hello, nice catch @semikernel!
> 
> I actually never use the OpenGL renderer, but I guess supporting it would be very easy.
> 
> Reading the `OpenGLCamera` class, it looks like I might need to try from `config["background_color"]` instead. Something similar might also be needed for the other attributes.
> 
> I don't have might time for this at the moment, but feel free to give it a try if you feel confident enough :-)

Hi,there! I figured it out by creating a ThreeDSlideGL class, it worked. I found it difficult to integrate it to manim-slides whole project. I need to change many **all = [ ]** stuffs. However, it worked well:)
```py
from manim_slides import Slide, ThreeDSlide
from manim.renderer.opengl_renderer import OpenGLCamera

class ThreeDSlideGL(ThreeDSlide):
    @property
    def _background_color(self) -> str:
        try:
            # Attempt to get the background color for OpenGLCamera
            if isinstance(self.camera, OpenGLCamera):
                # Check for possible attributes
                if hasattr(self.camera, 'background_rgba'):
                    color = self.camera.background_rgba
                elif hasattr(self.camera, 'background_color'):
                    color = self.camera.background_color
                else:
                    # Fallback to default color if none is found
                    color = (0, 0, 0, 1)  # Default to black
                
                return "#" + "".join(f"{int(c*255):02x}" for c in color[:3])
            else:
                # Handle other camera types
                color = self.camera.background_color
                if hex_color := getattr(color, "hex", None):
                    return hex_color  # type: ignore
                else:
                    return "#000000"  # Default to black if attribute is not found
        except Exception as e:
            # Handle any unexpected errors
            return "#000000"  # Default to black
```
> > Hello, nice catch @semikernel!
> > I actually never use the OpenGL renderer, but I guess supporting it would be very easy.
> > Reading the `OpenGLCamera` class, it looks like I might need to try from `config["background_color"]` instead. Something similar might also be needed for the other attributes.
> > I don't have might time for this at the moment, but feel free to give it a try if you feel confident enough :-)
> 
> Hi,there! I figured it out by creating a ThreeDSlideGL class, it worked. I found it difficult to integrate it to manim-slides whole project. I need to change many **all = [ ]** stuffs. However, it worked well:)
> 
> ```python
> from manim_slides import Slide, ThreeDSlide
> from manim.renderer.opengl_renderer import OpenGLCamera
> 
> class ThreeDSlideGL(ThreeDSlide):
>     @property
>     def _background_color(self) -> str:
>         try:
>             # Attempt to get the background color for OpenGLCamera
>             if isinstance(self.camera, OpenGLCamera):
>                 # Check for possible attributes
>                 if hasattr(self.camera, 'background_rgba'):
>                     color = self.camera.background_rgba
>                 elif hasattr(self.camera, 'background_color'):
>                     color = self.camera.background_color
>                 else:
>                     # Fallback to default color if none is found
>                     color = (0, 0, 0, 1)  # Default to black
>                 
>                 return "#" + "".join(f"{int(c*255):02x}" for c in color[:3])
>             else:
>                 # Handle other camera types
>                 color = self.camera.background_color
>                 if hex_color := getattr(color, "hex", None):
>                     return hex_color  # type: ignore
>                 else:
>                     return "#000000"  # Default to black if attribute is not found
>         except Exception as e:
>             # Handle any unexpected errors
>             return "#000000"  # Default to black
> ```

Thanks for sharing your solution, and sorry for the late reply!

The `config["background_color"]` didn't work? Because, for compatibility with future versions, I would like to prefer to have the simplest solution. And, if multiple cases have to be handled, then documenting why they might exist is very important IMO :-)

Fix patch:
diff --git a/.gitignore b/.gitignore
index 09cdb112..873bd6bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@ paper/paper.pdf
 paper/media/
 
 # Others
+.coverage
 coverage.xml
 
 rendering_times.csv
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 61242288..3796c97f 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -15,4 +15,7 @@ sphinx:
   fail_on_warning: true
 python:
   install:
-  - requirements: requirements-dev.lock
+  - method: pip
+    path: .
+    extra_requirements:
+    - docs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e31dbb00..1ebb607f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   [samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
   is solved.
   [#432](https://github.com/jeertmans/manim-slides/pull/432)
+- Removed an old validation check that prevented setting `loop=True` with
+  `auto_next=True` on `next_slide()`
+  [#445](https://github.com/jeertmans/manim-slides/pull/445)
+- Improved (and fixed) tests for Manim(GL), bumped minimal ManimCE version,
+  improved coverage, and override dependency conflicts.
+  [#447](https://github.com/jeertmans/manim-slides/pull/447)
 
 (unreleased-fixed)=
 ### Fixed
@@ -28,12 +34,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed whitespace issue in default RevealJS template.
   [#442](https://github.com/jeertmans/manim-slides/pull/442)
 
-(unreleased-chore)=
-### Chore
+(unreleased-removed)=
+### Removed
 
-- Removed an old validation check that prevented setting `loop=True` with
-  `auto_next=True` on `next_slide()`
-  [#445](https://github.com/jeertmans/manim-slides/pull/445)
+- Removed `full-gl` extra, because it does not make sense to ship both
+  `manimgl` and `manim` together.
+  [#447](https://github.com/jeertmans/manim-slides/pull/447)
 
 (v5.1.7)=
 ## [v5.1.7](https://github.com/jeertmans/manim-slides/compare/v5.1.6...v5.1.7)
diff --git a/docs/source/gallery.md b/docs/source/gallery.md
index b9941ffb..6b7d6641 100644
--- a/docs/source/gallery.md
+++ b/docs/source/gallery.md
@@ -23,10 +23,10 @@ using Manim Slides presentations.
 
 Daniel publishes his presentations on *Cosmology, String Theory and related*
 topics on his
-[personal website](https://panopepino.github.io/web_page/main_page/slides.html).
+[personal website](https://panopepino.github.io/Web_Page/main_page/slides.html).
 
 For example, below are the slides of a seminar he gave titled
-[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
+[Our Universe on a (Dark) Bubble](https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html).
 
 <div style="position:relative;padding-bottom:56.25%;">
     <iframe
diff --git a/docs/source/installation.md b/docs/source/installation.md
index f12f447e..313c4658 100644
--- a/docs/source/installation.md
+++ b/docs/source/installation.md
@@ -108,8 +108,6 @@ using optional dependencies:
 
 - `full`, to include `magic`, `manim`, and
   `sphinx-directive` extras (see below);
-- `full-gl`, to include `magic`, `manimgl`, and
-  `sphinx-directive` extras (see below);
 - `magic`, to include a Jupyter magic to render
   animations inside notebooks. This automatically installs `manim`,
   and does not work with ManimGL;
diff --git a/manim_slides/slide/manim.py b/manim_slides/slide/manim.py
index c72bd7ff..d3db2355 100644
--- a/manim_slides/slide/manim.py
+++ b/manim_slides/slide/manim.py
@@ -2,6 +2,8 @@
 from typing import Any, Optional
 
 from manim import Scene, ThreeDScene, config
+from manim.renderer.opengl_renderer import OpenGLRenderer
+from manim.utils.color import rgba_to_color
 
 from ..config import BaseSlideConfig
 from .base import BaseSlide
@@ -13,25 +15,40 @@ class Slide(BaseSlide, Scene):  # type: ignore[misc]
     for slides rendering.
     """
 
+    @property
+    def _frame_shape(self) -> tuple[float, float]:
+        if isinstance(self.renderer, OpenGLRenderer):
+            return self.renderer.camera.frame_shape  # type: ignore
+        else:
+            return (
+                self.renderer.camera.frame_height,
+                self.renderer.camera.frame_width,
+            )
+
     @property
     def _frame_height(self) -> float:
-        return config["frame_height"]  # type: ignore
+        return self._frame_shape[0]
 
     @property
     def _frame_width(self) -> float:
-        return config["frame_width"]  # type: ignore
+        return self._frame_shape[1]
 
     @property
     def _background_color(self) -> str:
-        color = self.camera.background_color
-        if hex_color := getattr(color, "hex", None):
-            return hex_color  # type: ignore
-        else:  # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
-            return color.to_hex()  # type: ignore
+        if isinstance(self.renderer, OpenGLRenderer):
+            return rgba_to_color(self.renderer.background_color).to_hex()  # type: ignore
+        else:
+            return self.renderer.camera.background_color.to_hex()  # type: ignore
 
     @property
     def _resolution(self) -> tuple[int, int]:
-        return config["pixel_width"], config["pixel_height"]
+        if isinstance(self.renderer, OpenGLRenderer):
+            return self.renderer.get_pixel_shape()  # type: ignore
+        else:
+            return (
+                self.renderer.camera.pixel_width,
+                self.renderer.camera.pixel_height,
+            )
 
     @property
     def _partial_movie_files(self) -> list[Path]:
diff --git a/pyproject.toml b/pyproject.toml
index 9784e5fe..28d99549 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,20 +42,35 @@ name = "manim-slides"
 requires-python = ">=3.9"
 
 [project.optional-dependencies]
+docs = [
+  "manim-slides[magic,manim,pyqt6,sphinx-directive]",
+  "furo>=2023.5.20",
+  "ipykernel>=6.25.1",
+  "myst-parser>=2.0.0",
+  "nbsphinx>=0.9.2",
+  "pandoc>=2.3",
+  "sphinx>=7.0.1",
+  "sphinx-click>=4.4.0",
+  "sphinx-copybutton>=0.5.1",
+  "sphinxext-opengraph>=0.7.5",
+]
 full = [
   "manim-slides[magic,manim,sphinx-directive]",
 ]
-full-gl = [
-  "manim-slides[magic,manimgl,sphinx-directive]",
-]
 magic = ["manim-slides[manim]", "ipython>=8.12.2"]
-manim = ["manim>=0.17.3"]
+manim = ["manim>=0.18.0"]
 manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
 pyqt6 = ["pyqt6>=6.6.1"]
 pyqt6-full = ["manim-slides[full,pyqt6]"]
-pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
+pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'", "pyside6>=6.6.1;python_version>='3.12'"]
 pyside6-full = ["manim-slides[full,pyside6]"]
 sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
+tests = [
+  "pytest>=7.4.0",
+  "pytest-cov>=4.1.0",
+  "pytest-env>=0.8.2",
+  "pytest-qt>=4.2.0",
+]
 
 [project.scripts]
 manim-slides = "manim_slides.__main__:cli"
@@ -161,13 +176,14 @@ python_version = "3.9"
 strict = true
 
 [tool.pytest.ini_options]
+addopts = [
+  "--cov-report=xml",
+  "--cov=manim_slides",
+]
 env = [
+  "QT_API=pyside6",
   "QT_QPA_PLATFORM=offscreen",
 ]
-filterwarnings = [
-  "error",
-  "ignore::DeprecationWarning",
-]
 
 [tool.ruff]
 extend-exclude = ["manim_slides/resources.py"]
@@ -193,27 +209,20 @@ extend-ignore = [
 extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
 isort = {known-first-party = ["manim_slides", "tests"]}
 
+[tool.ruff.lint.per-file-ignores]
+"tests/test_slide.py" = ["N801"]
+
 [tool.rye]
 dev-dependencies = [
-  "manim-slides[magic,manim,manimgl,pyqt6,sphinx-directive]",
-  # dev
   "bump-my-version>=0.20.3",
   "pre-commit>=3.5.0",
-  # docs
-  "furo>=2023.5.20",
-  "ipykernel>=6.25.1",
-  "myst-parser>=2.0.0",
-  "nbsphinx>=0.9.2",
-  "pandoc>=2.3",
-  "sphinx>=7.0.1",
-  "sphinx-click>=4.4.0",
-  "sphinx-copybutton>=0.5.1",
-  "sphinxext-opengraph>=0.7.5",
-  # tests
-  "pytest>=7.4.0",
-  "pytest-cov>=4.1.0",
-  "pytest-env>=0.8.2",
-  "pytest-qt>=4.2.0",
-  "pytest-xdist>=3.3.1",
 ]
 managed = true
+
+[tool.uv]
+override-dependencies = [
+  # Bypass constraints from ManimGL
+  "manimpango>=0.5.0,<1.0.0",
+  "numpy<=1.24;python_version < '3.12'",
+  "numpy>=1.26;python_version >= '3.12'",
+]
diff --git a/requirements-dev.lock b/requirements-dev.lock
index fab4d63e..3803e098 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -6,6 +6,7 @@
 #   features: []
 #   all-features: true
 #   with-sources: false
+#   generate-hashes: false
 
 -e file:.
 alabaster==0.7.16
@@ -44,12 +45,10 @@ click==8.1.7
     # via rich-click
     # via sphinx-click
 click-default-group==1.2.4
-    # via manim
     # via manim-slides
-cloup==0.13.1
+cloup==3.0.5
     # via manim
 colour==0.1.5
-    # via manim
     # via manimgl
 comm==0.2.2
     # via ipykernel
@@ -74,17 +73,16 @@ docutils==0.20.1
     # via nbsphinx
     # via sphinx
     # via sphinx-click
-execnet==2.1.1
-    # via pytest-xdist
 executing==2.0.1
     # via stack-data
 fastjsonschema==2.19.1
     # via nbformat
 filelock==3.13.4
     # via virtualenv
-fonttools==4.51.0
+fonttools==4.53.0
     # via matplotlib
 furo==2024.1.29
+    # via manim-slides
 glcontext==2.5.0
     # via moderngl
 identify==2.5.35
@@ -96,6 +94,7 @@ imagesize==1.4.1
 iniconfig==2.0.0
     # via pytest
 ipykernel==6.29.4
+    # via manim-slides
 ipython==8.18.1
     # via ipykernel
     # via manim-slides
@@ -131,11 +130,12 @@ kiwisolver==1.4.5
 lxml==5.2.1
     # via manim-slides
     # via python-pptx
-manim==0.17.3
+manim==0.18.1
     # via manim-slides
 manimgl==1.6.1
     # via manim-slides
-manimpango==0.4.4
+manimpango==0.5.0
+    # via --override (workspace)
     # via manim
     # via manimgl
 mapbox-earcut==1.0.1
@@ -148,7 +148,7 @@ markdown-it-py==3.0.0
 markupsafe==2.1.5
     # via jinja2
     # via nbconvert
-matplotlib==3.8.4
+matplotlib==3.9.0
     # via manimgl
 matplotlib-inline==0.1.7
     # via ipykernel
@@ -163,7 +163,7 @@ moderngl==5.10.0
     # via manim
     # via manimgl
     # via moderngl-window
-moderngl-window==2.4.4
+moderngl-window==2.4.6
     # via manim
     # via manimgl
 mpmath==1.3.0
@@ -171,6 +171,7 @@ mpmath==1.3.0
 multipledispatch==1.0.0
     # via pyrr
 myst-parser==2.0.0
+    # via manim-slides
 nbclient==0.10.0
     # via nbconvert
 nbconvert==7.16.3
@@ -180,14 +181,17 @@ nbformat==5.10.4
     # via nbconvert
     # via nbsphinx
 nbsphinx==0.9.3
+    # via manim-slides
 nest-asyncio==1.6.0
     # via ipykernel
 networkx==2.8.8
     # via manim
 nodeenv==1.8.0
     # via pre-commit
-numpy==1.26.4
+numpy==1.24.0
+    # via --override (workspace)
     # via contourpy
+    # via ipython
     # via isosurfaces
     # via manim
     # via manim-slides
@@ -195,6 +199,7 @@ numpy==1.26.4
     # via mapbox-earcut
     # via matplotlib
     # via moderngl-window
+    # via networkx
     # via pyrr
     # via scipy
 packaging==24.0
@@ -205,13 +210,14 @@ packaging==24.0
     # via qtpy
     # via sphinx
 pandoc==2.3
+    # via manim-slides
 pandocfilters==1.5.1
     # via nbconvert
 parso==0.8.4
     # via jedi
 pexpect==4.9.0
     # via ipython
-pillow==9.5.0
+pillow==10.3.0
     # via manim
     # via manim-slides
     # via manimgl
@@ -284,14 +290,16 @@ pyside6-essentials==6.5.2
     # via pyside6
     # via pyside6-addons
 pytest==8.1.1
+    # via manim-slides
     # via pytest-cov
     # via pytest-env
     # via pytest-qt
-    # via pytest-xdist
 pytest-cov==5.0.0
+    # via manim-slides
 pytest-env==1.1.3
+    # via manim-slides
 pytest-qt==4.4.0
-pytest-xdist==3.5.0
+    # via manim-slides
 python-dateutil==2.9.0.post0
     # via jupyter-client
     # via matplotlib
@@ -314,7 +322,6 @@ referencing==0.34.0
     # via jsonschema
     # via jsonschema-specifications
 requests==2.31.0
-    # via manim
     # via manim-slides
     # via sphinx
 rich==13.7.1
@@ -346,7 +353,7 @@ six==1.16.0
     # via asttokens
     # via bleach
     # via python-dateutil
-skia-pathops==0.7.4
+skia-pathops==0.8.0.post1
     # via manim
     # via manimgl
 snowballstemmer==2.2.0
@@ -355,6 +362,7 @@ soupsieve==2.5
     # via beautifulsoup4
 sphinx==7.3.6
     # via furo
+    # via manim-slides
     # via myst-parser
     # via nbsphinx
     # via sphinx-basic-ng
@@ -364,7 +372,9 @@ sphinx==7.3.6
 sphinx-basic-ng==1.0.0b2
     # via furo
 sphinx-click==5.1.0
+    # via manim-slides
 sphinx-copybutton==0.5.2
+    # via manim-slides
 sphinxcontrib-applehelp==1.0.8
     # via sphinx
 sphinxcontrib-devhelp==1.0.6
@@ -378,6 +388,7 @@ sphinxcontrib-qthelp==1.0.7
 sphinxcontrib-serializinghtml==1.1.10
     # via sphinx
 sphinxext-opengraph==0.9.1
+    # via manim-slides
 srt==3.5.3
     # via manim
 stack-data==0.6.3
@@ -385,7 +396,7 @@ stack-data==0.6.3
 svgelements==1.9.6
     # via manim
     # via manimgl
-sympy==1.12
+sympy==1.12.1
     # via manimgl
 tinycss2==1.2.1
     # via nbconvert
@@ -410,12 +421,13 @@ traitlets==5.14.2
     # via nbformat
     # via nbsphinx
 typing-extensions==4.11.0
+    # via manim
     # via pydantic
     # via pydantic-core
     # via rich-click
 urllib3==2.2.1
     # via requests
-validators==0.28.0
+validators==0.28.3
     # via manimgl
 virtualenv==20.25.3
     # via pre-commit
diff --git a/requirements.lock b/requirements.lock
index db09e50c..0689223c 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -6,14 +6,27 @@
 #   features: []
 #   all-features: true
 #   with-sources: false
+#   generate-hashes: false
 
 -e file:.
+alabaster==0.7.16
+    # via sphinx
 annotated-types==0.6.0
     # via pydantic
 asttokens==2.4.1
     # via stack-data
+attrs==23.2.0
+    # via jsonschema
+    # via referencing
 av==12.0.0
     # via manim-slides
+babel==2.15.0
+    # via sphinx
+beautifulsoup4==4.12.3
+    # via furo
+    # via nbconvert
+bleach==6.1.0
+    # via nbconvert
 certifi==2024.2.2
     # via requests
 charset-normalizer==3.3.2
@@ -23,32 +36,54 @@ click==8.1.7
     # via cloup
     # via manim
     # via manim-slides
+    # via sphinx-click
 click-default-group==1.2.4
-    # via manim
     # via manim-slides
-cloup==0.13.1
+cloup==3.0.5
     # via manim
 colour==0.1.5
-    # via manim
     # via manimgl
+comm==0.2.2
+    # via ipykernel
 contourpy==1.2.1
     # via matplotlib
+coverage==7.5.4
+    # via pytest-cov
 cycler==0.12.1
     # via matplotlib
+debugpy==1.8.2
+    # via ipykernel
 decorator==5.1.1
     # via ipython
     # via manim
+defusedxml==0.7.1
+    # via nbconvert
 docutils==0.20.1
     # via manim-slides
+    # via myst-parser
+    # via nbsphinx
+    # via sphinx
+    # via sphinx-click
 executing==2.0.1
     # via stack-data
-fonttools==4.51.0
+fastjsonschema==2.20.0
+    # via nbformat
+fonttools==4.53.0
     # via matplotlib
+furo==2024.5.6
+    # via manim-slides
 glcontext==2.5.0
     # via moderngl
 idna==3.7
     # via requests
+imagesize==1.4.1
+    # via sphinx
+iniconfig==2.0.0
+    # via pytest
+ipykernel==6.29.4
+    # via manim-slides
 ipython==8.18.1
+    # via ipykernel
     # via manim-slides
     # via manimgl
 isosurfaces==0.1.0
@@ -58,46 +93,90 @@ jedi==0.19.1
     # via ipython
 jinja2==3.1.3
     # via manim-slides
+    # via myst-parser
+    # via nbconvert
+    # via nbsphinx
+    # via sphinx
+jsonschema==4.22.0
+    # via nbformat
+jsonschema-specifications==2023.12.1
+    # via jsonschema
+jupyter-client==8.6.2
+    # via ipykernel
+    # via nbclient
+jupyter-core==5.7.2
+    # via ipykernel
+    # via jupyter-client
+    # via nbclient
+    # via nbconvert
+    # via nbformat
+jupyterlab-pygments==0.3.0
+    # via nbconvert
 kiwisolver==1.4.5
     # via matplotlib
 lxml==5.2.1
     # via manim-slides
     # via python-pptx
-manim==0.17.3
+manim==0.18.1
     # via manim-slides
 manimgl==1.6.1
     # via manim-slides
-manimpango==0.4.4
+manimpango==0.5.0
+    # via --override (workspace)
     # via manim
     # via manimgl
 mapbox-earcut==1.0.1
     # via manim
     # via manimgl
 markdown-it-py==3.0.0
+    # via mdit-py-plugins
+    # via myst-parser
     # via rich
 markupsafe==2.1.5
     # via jinja2
-matplotlib==3.8.4
+    # via nbconvert
+matplotlib==3.9.0
     # via manimgl
 matplotlib-inline==0.1.7
+    # via ipykernel
     # via ipython
+mdit-py-plugins==0.4.1
+    # via myst-parser
 mdurl==0.1.2
     # via markdown-it-py
+mistune==3.0.2
+    # via nbconvert
 moderngl==5.10.0
     # via manim
     # via manimgl
     # via moderngl-window
-moderngl-window==2.4.4
+moderngl-window==2.4.6
     # via manim
     # via manimgl
 mpmath==1.3.0
     # via sympy
 multipledispatch==1.0.0
     # via pyrr
+myst-parser==3.0.1
+    # via manim-slides
+nbclient==0.10.0
+    # via nbconvert
+nbconvert==7.16.4
+    # via nbsphinx
+nbformat==5.10.4
+    # via nbclient
+    # via nbconvert
+    # via nbsphinx
+nbsphinx==0.9.4
+    # via manim-slides
+nest-asyncio==1.6.0
+    # via ipykernel
 networkx==2.8.8
     # via manim
-numpy==1.26.4
+numpy==1.24.0
+    # via --override (workspace)
     # via contourpy
+    # via ipython
     # via isosurfaces
     # via manim
     # via manim-slides
@@ -105,24 +184,44 @@ numpy==1.26.4
     # via mapbox-earcut
     # via matplotlib
     # via moderngl-window
+    # via networkx
     # via pyrr
     # via scipy
 packaging==24.0
+    # via ipykernel
     # via matplotlib
+    # via nbconvert
+    # via pytest
     # via qtpy
+    # via sphinx
+pandoc==2.3
+    # via manim-slides
+pandocfilters==1.5.1
+    # via nbconvert
 parso==0.8.4
     # via jedi
 pexpect==4.9.0
     # via ipython
-pillow==9.5.0
+pillow==10.3.0
     # via manim
     # via manim-slides
     # via manimgl
     # via matplotlib
     # via moderngl-window
     # via python-pptx
+platformdirs==4.2.2
+    # via jupyter-core
+pluggy==1.5.0
+    # via pytest
+    # via pytest-qt
+plumbum==1.8.3
+    # via pandoc
+ply==3.11
+    # via pandoc
 prompt-toolkit==3.0.43
     # via ipython
+psutil==6.0.0
+    # via ipykernel
 ptyprocess==0.7.0
     # via pexpect
 pure-eval==0.2.2
@@ -142,10 +241,13 @@ pydub==0.25.1
 pyglet==2.0.15
     # via moderngl-window
 pygments==2.17.2
+    # via furo
     # via ipython
     # via manim
     # via manimgl
+    # via nbconvert
     # via rich
+    # via sphinx
 pyopengl==3.1.7
     # via manimgl
 pyparsing==3.1.2
@@ -165,21 +267,43 @@ pyside6-addons==6.5.2
 pyside6-essentials==6.5.2
     # via pyside6
     # via pyside6-addons
+pytest==8.2.2
+    # via manim-slides
+    # via pytest-cov
+    # via pytest-env
+    # via pytest-qt
+pytest-cov==5.0.0
+    # via manim-slides
+pytest-env==1.1.3
+    # via manim-slides
+pytest-qt==4.4.0
+    # via manim-slides
 python-dateutil==2.9.0.post0
+    # via jupyter-client
     # via matplotlib
 python-pptx==0.6.23
     # via manim-slides
 pyyaml==6.0.1
     # via manimgl
+    # via myst-parser
+pyzmq==26.0.3
+    # via ipykernel
+    # via jupyter-client
 qtpy==2.4.1
     # via manim-slides
+referencing==0.35.1
+    # via jsonschema
+    # via jsonschema-specifications
 requests==2.31.0
-    # via manim
     # via manim-slides
+    # via sphinx
 rich==13.7.1
     # via manim
     # via manim-slides
     # via manimgl
+rpds-py==0.18.1
+    # via jsonschema
+    # via referencing
 rtoml==0.10.0
     # via manim-slides
 scipy==1.13.0
@@ -194,10 +318,44 @@ shiboken6==6.5.2
     # via pyside6-essentials
 six==1.16.0
     # via asttokens
+    # via bleach
     # via python-dateutil
-skia-pathops==0.7.4
+skia-pathops==0.8.0.post1
     # via manim
     # via manimgl
+snowballstemmer==2.2.0
+    # via sphinx
+soupsieve==2.5
+    # via beautifulsoup4
+sphinx==7.3.7
+    # via furo
+    # via manim-slides
+    # via myst-parser
+    # via nbsphinx
+    # via sphinx-basic-ng
+    # via sphinx-click
+    # via sphinx-copybutton
+    # via sphinxext-opengraph
+sphinx-basic-ng==1.0.0b2
+    # via furo
+sphinx-click==6.0.0
+    # via manim-slides
+sphinx-copybutton==0.5.2
+    # via manim-slides
+sphinxcontrib-applehelp==1.0.8
+    # via sphinx
+sphinxcontrib-devhelp==1.0.6
+    # via sphinx
+sphinxcontrib-htmlhelp==2.0.5
+    # via sphinx
+sphinxcontrib-jsmath==1.0.1
+    # via sphinx
+sphinxcontrib-qthelp==1.0.7
+    # via sphinx
+sphinxcontrib-serializinghtml==1.1.10
+    # via sphinx
+sphinxext-opengraph==0.9.1
+    # via manim-slides
 srt==3.5.3
     # via manim
 stack-data==0.6.3
@@ -205,25 +363,42 @@ stack-data==0.6.3
 svgelements==1.9.6
     # via manim
     # via manimgl
-sympy==1.12
+sympy==1.12.1
     # via manimgl
+tinycss2==1.3.0
+    # via nbconvert
+tornado==6.4.1
+    # via ipykernel
+    # via jupyter-client
 tqdm==4.66.2
     # via manim
     # via manim-slides
     # via manimgl
 traitlets==5.14.2
+    # via comm
+    # via ipykernel
     # via ipython
+    # via jupyter-client
+    # via jupyter-core
     # via matplotlib-inline
+    # via nbclient
+    # via nbconvert
+    # via nbformat
+    # via nbsphinx
 typing-extensions==4.11.0
+    # via manim
     # via pydantic
     # via pydantic-core
 urllib3==2.2.1
     # via requests
-validators==0.28.0
+validators==0.28.3
     # via manimgl
 watchdog==2.3.1
     # via manim
 wcwidth==0.2.13
     # via prompt-toolkit
+webencodings==0.5.1
+    # via bleach
+    # via tinycss2
 xlsxwriter==3.2.0
     # via python-pptx


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0420e363..5ed87a3f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,7 +7,52 @@ on:
 
 name: Tests
 
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  cancel-in-progress: true
+
 jobs:
+  pip-install:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [macos-latest, ubuntu-latest, windows-latest]
+        pyversion: ['3.9', '3.10', '3.11', '3.12']
+        extras: [pyside6-full, manimgl]
+        exclude:
+        - pyversion: '3.12'
+          extras: manimgl
+    runs-on: ${{ matrix.os }}
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+
+    - name: Install Python
+      uses: actions/setup-python@v5
+      with:
+        python-version: ${{ matrix.pyversion }}
+        cache: pip
+
+    - name: Install manim dependencies on MacOS
+      if: matrix.os == 'macos-latest'
+      run: brew install ffmpeg py3cairo pango pkg-config scipy
+
+    - name: Install manim dependencies on Ubuntu
+      if: matrix.os == 'ubuntu-latest'
+      run: |
+        sudo apt-get update
+        sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
+        nohup Xvfb $DISPLAY &
+
+    - name: Install Windows dependencies
+      if: matrix.os == 'windows-latest'
+      run: choco install ffmpeg
+
+    - name: Install package
+      shell: bash
+      env:
+        extras: ${{ matrix.extras }}
+      run: pip install ".[$extras]"
   pytest:
     strategy:
       fail-fast: false
@@ -74,15 +119,9 @@ jobs:
 
     - name: Run pytest
       shell: bash
-      if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
       run: rye run pytest
 
-    - name: Run pytest and coverage
-      if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
-      run: rye run pytest --cov-report xml --cov=manim_slides tests/
-
     - name: Upload to codecov.io
-      if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
       uses: codecov/codecov-action@v4
       env:
         CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/tests/test_slide.py b/tests/test_slide.py
index 05e0809c..10cbdcb4 100644
--- a/tests/test_slide.py
+++ b/tests/test_slide.py
@@ -2,13 +2,11 @@
 import shutil
 import sys
 from pathlib import Path
+from typing import Any, Union
 
-import manim
-import numpy as np
 import pytest
 from click.testing import CliRunner
 from manim import (
-    BLACK,
     BLUE,
     DOWN,
     LEFT,
@@ -21,12 +19,30 @@
     GrowFromCenter,
     Text,
 )
-from packaging import version
+from manim.renderer.opengl_renderer import OpenGLRenderer
 
 from manim_slides.config import PresentationConfig
 from manim_slides.defaults import FOLDER_PATH
 from manim_slides.render import render
-from manim_slides.slide.manim import Slide
+from manim_slides.slide.manim import Slide as CESlide
+
+
+class CEGLSlide(CESlide):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
+
+
+if sys.version_info >= (3, 12):
+
+    class _GLSlide:
+        pass
+
+    GLSlide = pytest.param(_GLSlide, marks=pytest.mark.skip())
+else:
+    from manim_slides.slide.manimlib import Slide as GLSlide
+
+SlideType = Union[type[CESlide], type[GLSlide], type[CEGLSlide]]
+Slide = Union[CESlide, GLSlide, CEGLSlide]
 
 
 @pytest.mark.parametrize(
@@ -36,8 +52,7 @@
         pytest.param(
             "--GL",
             marks=pytest.mark.skipif(
-                version.parse(np.__version__) >= version.parse("1.25")
-                or sys.version_info >= (3, 12),
+                sys.version_info >= (3, 12),
                 reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
             ),
         ),
@@ -82,271 +97,315 @@ def test_render_basic_slide(
         assert local_presentation_config.resolution == presentation_config.resolution
 
 
-def assert_constructs(cls: type) -> type:
-    class Wrapper:
-        @classmethod
-        def test_construct(_) -> None:  # noqa: N804
-            cls().construct()
+def init_slide(cls: SlideType) -> Slide:
+    if issubclass(cls, CESlide):
+        return cls()
+    elif issubclass(cls, GLSlide):
+        from manimlib.config import get_configuration, parse_cli
+        from manimlib.extract_scene import get_scene_config
 
-    return Wrapper
+        args = parse_cli()
+        config = get_configuration(args)
+        scene_config = get_scene_config(config)
+        return cls(**scene_config)
 
+    raise ValueError(f"Unsupported class {cls}")
 
-def assert_renders(cls: type) -> type:
-    class Wrapper:
-        @classmethod
-        def test_render(_) -> None:  # noqa: N804
-            cls().render()
 
-    return Wrapper
+parametrize_base_cls = pytest.mark.parametrize(
+    "base_cls", (CESlide, GLSlide, CEGLSlide), ids=("CE", "GL", "CE(GL)")
+)
 
 
-class TestSlide:
-    @assert_constructs
-    class TestDefaultProperties(Slide):
-        def construct(self) -> None:
-            assert self._output_folder == FOLDER_PATH
-            assert len(self._slides) == 0
-            assert self._current_slide == 1
-            assert self._start_animation == 0
-            assert len(self._canvas) == 0
-            assert self._wait_time_between_slides == 0.0
+def assert_constructs(cls: SlideType) -> None:
+    init_slide(cls).construct()
 
-    @pytest.mark.skipif(
-        version.parse(manim.__version__) < version.parse("0.18"),
-        reason="Manim change how color are represented in 0.18",
-    )
-    @assert_constructs
-    class TestBackgroundColor(Slide):
-        def construct(self) -> None:
-            assert self._background_color == BLACK.to_hex()  # DEFAULT
-            self.camera.background_color = BLUE
-            assert self._background_color == BLUE.to_hex()
 
-    @assert_renders
-    class TestMultipleAnimationsInLastSlide(Slide):
-        """Check against solution for issue #161."""
+def assert_renders(cls: SlideType) -> None:
+    init_slide(cls).render()
 
-        def construct(self) -> None:
-            circle = Circle(color=BLUE)
-            dot = Dot()
 
-            self.play(GrowFromCenter(circle))
-            self.play(FadeIn(dot))
-            self.next_slide()
+class TestSlide:
+    def test_default_properties(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                assert self._output_folder == FOLDER_PATH
+                assert len(self._slides) == 0
+                assert self._current_slide == 1
+                assert self._start_animation == 0
+                assert len(self._canvas) == 0
+                assert self._wait_time_between_slides == 0.0
+
+    @parametrize_base_cls
+    def test_frame_height(self, base_cls: SlideType) -> None:
+        @assert_constructs
+        class _(base_cls):  # type: ignore
+            def construct(self) -> None:
+                assert self._frame_height > 0 and isinstance(self._frame_height, float)
+
+    @parametrize_base_cls
+    def test_frame_width(self, base_cls: SlideType) -> None:
+        @assert_constructs
+        class _(base_cls):  # type: ignore
+            def construct(self) -> None:
+                assert self._frame_width > 0 and isinstance(self._frame_width, float)
+
+    @parametrize_base_cls
+    def test_resolution(self, base_cls: SlideType) -> None:
+        @assert_constructs
+        class _(base_cls):  # type: ignore
+            def construct(self) -> None:
+                pw, ph = self._resolution
+                assert isinstance(pw, int) and pw > 0
+                assert isinstance(ph, int) and ph > 0
+
+    @parametrize_base_cls
+    def test_backround_color(self, base_cls: SlideType) -> None:
+        @assert_constructs
+        class _(base_cls):  # type: ignore
+            def construct(self) -> None:
+                assert self._background_color in ["#000000", "#000"]  # DEFAULT
+
+    def test_multiple_animations_in_last_slide(self) -> None:
+        @assert_renders
+        class _(CESlide):
+            """Check against solution for issue #161."""
+
+            def construct(self) -> None:
+                circle = Circle(color=BLUE)
+                dot = Dot()
+
+                self.play(GrowFromCenter(circle))
+                self.play(FadeIn(dot))
+                self.next_slide()
 
-            self.play(dot.animate.move_to(RIGHT))
-            self.play(dot.animate.move_to(UP))
-            self.play(dot.animate.move_to(LEFT))
-            self.play(dot.animate.move_to(DOWN))
+                self.play(dot.animate.move_to(RIGHT))
+                self.play(dot.animate.move_to(UP))
+                self.play(dot.animate.move_to(LEFT))
+                self.play(dot.animate.move_to(DOWN))
 
-    @assert_renders
-    class TestFileTooLong(Slide):
-        """Check against solution for issue #123."""
+    def test_file_too_long(self) -> None:
+        @assert_renders
+        class _(CESlide):
+            """Check against solution for issue #123."""
 
-        def construct(self) -> None:
-            circle = Circle(radius=3, color=BLUE)
-            dot = Dot()
-            self.play(GrowFromCenter(circle), run_time=0.1)
+            def construct(self) -> None:
+                circle = Circle(radius=3, color=BLUE)
+                dot = Dot()
+                self.play(GrowFromCenter(circle), run_time=0.1)
 
-            for _ in range(30):
-                direction = (random.random() - 0.5) * LEFT + (
-                    random.random() - 0.5
-                ) * UP
-                self.play(dot.animate.move_to(direction), run_time=0.1)
-                self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
+                for _ in range(30):
+                    direction = (random.random() - 0.5) * LEFT + (
+                        random.random() - 0.5
+                    ) * UP
+                    self.play(dot.animate.move_to(direction), run_time=0.1)
+                    self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
 
-    @assert_constructs
-    class TestLoop(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
+    def test_loop(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
 
-            self.add(text)
+                self.add(text)
 
-            assert not self._base_slide_config.loop
+                assert not self._base_slide_config.loop
 
-            self.next_slide(loop=True)
-            self.play(text.animate.scale(2))
+                self.next_slide(loop=True)
+                self.play(text.animate.scale(2))
 
-            assert self._base_slide_config.loop
+                assert self._base_slide_config.loop
 
-            self.next_slide(loop=False)
+                self.next_slide(loop=False)
 
-            assert not self._base_slide_config.loop
+                assert not self._base_slide_config.loop
 
-    @assert_constructs
-    class TestAutoNext(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
+    def test_auto_next(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
 
-            self.add(text)
+                self.add(text)
 
-            assert not self._base_slide_config.auto_next
+                assert not self._base_slide_config.auto_next
 
-            self.next_slide(auto_next=True)
-            self.play(text.animate.scale(2))
+                self.next_slide(auto_next=True)
+                self.play(text.animate.scale(2))
 
-            assert self._base_slide_config.auto_next
+                assert self._base_slide_config.auto_next
 
-            self.next_slide(auto_next=False)
+                self.next_slide(auto_next=False)
 
-            assert not self._base_slide_config.auto_next
+                assert not self._base_slide_config.auto_next
 
-    @assert_constructs
-    class TestLoopAndAutoNextSucceeds(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
+    def test_loop_and_auto_next_succeeds(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
 
-            self.add(text)
+                self.add(text)
 
-            self.next_slide(loop=True, auto_next=True)
-            self.play(text.animate.scale(2))
+                self.next_slide(loop=True, auto_next=True)
+                self.play(text.animate.scale(2))
 
-            self.next_slide()
+                self.next_slide()
 
-    @assert_constructs
-    class TestPlaybackRate(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
+    def test_playback_rate(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
 
-            self.add(text)
+                self.add(text)
 
-            assert self._base_slide_config.playback_rate == 1.0
+                assert self._base_slide_config.playback_rate == 1.0
 
-            self.next_slide(playback_rate=2.0)
-            self.play(text.animate.scale(2))
+                self.next_slide(playback_rate=2.0)
+                self.play(text.animate.scale(2))
 
-            assert self._base_slide_config.playback_rate == 2.0
+                assert self._base_slide_config.playback_rate == 2.0
 
-    @assert_constructs
-    class TestReversedPlaybackRate(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
+    def test_reversed_playback_rate(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
 
-            self.add(text)
+                self.add(text)
 
-            assert self._base_slide_config.reversed_playback_rate == 1.0
+                assert self._base_slide_config.reversed_playback_rate == 1.0
 
-            self.next_slide(reversed_playback_rate=2.0)
-            self.play(text.animate.scale(2))
+                self.next_slide(reversed_playback_rate=2.0)
+                self.play(text.animate.scale(2))
 
-            assert self._base_slide_config.reversed_playback_rate == 2.0
+                assert self._base_slide_config.reversed_playback_rate == 2.0
 
-    @assert_constructs
-    class TestNotes(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
+    def test_notes(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
 
-            self.add(text)
+                self.add(text)
 
-            assert self._base_slide_config.notes == ""
+                assert self._base_slide_config.notes == ""
 
-            self.next_slide(notes="test")
-            self.play(text.animate.scale(2))
+                self.next_slide(notes="test")
+                self.play(text.animate.scale(2))
 
-            assert self._base_slide_config.notes == "test"
+                assert self._base_slide_config.notes == "test"
 
-    @assert_constructs
-    class TestWipe(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
-            bye = Text("Bye")
+    def test_wipe(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
+                bye = Text("Bye")
 
-            self.add(text)
+                self.add(text)
 
-            assert text in self.mobjects
-            assert bye not in self.mobjects
+                assert text in self.mobjects
+                assert bye not in self.mobjects
 
-            self.wipe([text], [bye])
+                self.wipe([text], [bye])
 
-            assert text not in self.mobjects
-            assert bye in self.mobjects
+                assert text not in self.mobjects
+                assert bye in self.mobjects
 
-    @assert_constructs
-    class TestZoom(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
-            bye = Text("Bye")
+    def test_zoom(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
+                bye = Text("Bye")
 
-            self.add(text)
+                self.add(text)
 
-            assert text in self.mobjects
-            assert bye not in self.mobjects
+                assert text in self.mobjects
+                assert bye not in self.mobjects
 
-            self.zoom([text], [bye])
+                self.zoom([text], [bye])
 
-            assert text not in self.mobjects
-            assert bye in self.mobjects
+                assert text not in self.mobjects
+                assert bye in self.mobjects
 
-    @assert_constructs
-    class TestPlay(Slide):
-        def construct(self) -> None:
-            assert self._current_animation == 0
-            circle = Circle(color=BLUE)
-            dot = Dot()
+    def test_animation_count(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                assert self._current_animation == 0
+                circle = Circle(color=BLUE)
+                dot = Dot()
 
-            self.play(GrowFromCenter(circle))
-            assert self._current_animation == 1
-            self.play(FadeIn(dot))
-            assert self._current_animation == 2
+                self.play(GrowFromCenter(circle))
+                assert self._current_animation == 1
+                self.play(FadeIn(dot))
+                assert self._current_animation == 2
 
-    @assert_constructs
-    class TestWaitTimeBetweenSlides(Slide):
-        def construct(self) -> None:
-            self._wait_time_between_slides = 1.0
-            assert self._current_animation == 0
-            circle = Circle(color=BLUE)
-            self.play(GrowFromCenter(circle))
-            assert self._current_animation == 1
-            self.next_slide()
-            assert self._current_animation == 2  # self.wait = +1
+    def test_wait_time_between_slides(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                self._wait_time_between_slides = 1.0
+                assert self._current_animation == 0
+                circle = Circle(color=BLUE)
+                self.play(GrowFromCenter(circle))
+                assert self._current_animation == 1
+                self.next_slide()
+                assert self._current_animation == 2  # self.wait = +1
 
-    @assert_constructs
-    class TestNextSlide(Slide):
-        def construct(self) -> None:
-            assert self._current_slide == 1
-            self.next_slide()
-            assert self._current_slide == 1
-            circle = Circle(color=BLUE)
-            self.play(GrowFromCenter(circle))
-            self.next_slide()
-            assert self._current_slide == 2
-            self.next_slide()
-            assert self._current_slide == 2
+    def test_next_slide(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                assert self._current_slide == 1
+                self.next_slide()
+                assert self._current_slide == 1
+                circle = Circle(color=BLUE)
+                self.play(GrowFromCenter(circle))
+                self.next_slide()
+                assert self._current_slide == 2
+                self.next_slide()
+                assert self._current_slide == 2
 
-    @assert_constructs
-    class TestCanvas(Slide):
-        def construct(self) -> None:
-            text = Text("Some text")
-            bye = Text("Bye")
+    def test_canvas(self) -> None:
+        @assert_constructs
+        class _(CESlide):
+            def construct(self) -> None:
+                text = Text("Some text")
+                bye = Text("Bye")
 
-            assert len(self.canvas) == 0
+                assert len(self.canvas) == 0
 
-            self.add(text)
+                self.add(text)
 
-            assert len(self.canvas) == 0
+                assert len(self.canvas) == 0
 
-            self.add_to_canvas(text=text)
+                self.add_to_canvas(text=text)
 
-            assert len(self.canvas) == 1
+                assert len(self.canvas) == 1
 
-            self.add(bye)
+                self.add(bye)
 
-            assert len(self.canvas) == 1
+                assert len(self.canvas) == 1
 
-            assert text not in self.mobjects_without_canvas
-            assert bye in self.mobjects_without_canvas
+                assert text not in self.mobjects_without_canvas
+                assert bye in self.mobjects_without_canvas
 
-            self.remove(text)
+                self.remove(text)
 
-            assert len(self.canvas) == 1
+                assert len(self.canvas) == 1
 
-            self.add_to_canvas(bye=bye)
+                self.add_to_canvas(bye=bye)
 
-            assert len(self.canvas) == 2
+                assert len(self.canvas) == 2
 
-            self.remove_from_canvas("text", "bye")
+                self.remove_from_canvas("text", "bye")
 
-            assert len(self.canvas) == 0
+                assert len(self.canvas) == 0
 
-            with pytest.raises(KeyError):
-                self.remove_from_canvas("text")
+                with pytest.raises(KeyError):
+                    self.remove_from_canvas("text")