chore(tests): adding tests for Qt widgets (#300)

* chore(tests): adding tests for Qt widgets

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: reset WINDOW name

* chore(tests): addign tests

* chore: adding more tests

* fix how bin existence is checked

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix: which takes str

* more tests!

* todo: fix

* change verbosity in tests

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jérome Eertmans
2023-10-27 16:40:05 +02:00
committed by GitHub
parent 760ceb8ce1
commit 2853ed08e1
18 changed files with 605 additions and 88 deletions

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug MANIM_SLIDES_VERBOSITY: error
PYTHONFAULTHANDLER: 1 PYTHONFAULTHANDLER: 1
DISPLAY: :99 DISPLAY: :99
GITHUB_WORKFLOWS: 1 GITHUB_WORKFLOWS: 1

View File

@ -46,6 +46,8 @@ In an effort to better document changes, this CHANGELOG document is now created.
- Added `Slide.next_section` for compatibility with `manim`'s - Added `Slide.next_section` for compatibility with `manim`'s
`Scene.next_section` method. `Scene.next_section` method.
[#295](https://github.com/jeertmans/manim-slides/pull/295) [#295](https://github.com/jeertmans/manim-slides/pull/295)
- Added `--playback-rate` option to `manim-slides present` for testing purposes.
[#300](https://github.com/jeertmans/manim-slides/pull/300)
(v5-changed)= (v5-changed)=
### Changed ### Changed

View File

@ -7,11 +7,11 @@ import click
from click import Context, Parameter from click import Context, Parameter
from pydantic import ValidationError from pydantic import ValidationError
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication
from ..commons import config_path_option, folder_path_option, verbosity_option from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig from ..config import Config, PresentationConfig
from ..logger import logger from ..logger import logger
from ..qt_utils import qapp
from .player import Player from .player import Player
ASPECT_RATIO_MODES = { ASPECT_RATIO_MODES = {
@ -210,7 +210,14 @@ def start_at_callback(
metavar="NUMBER", metavar="NUMBER",
type=int, type=int,
default=None, default=None,
help="Presents content on the given screen (a.k.a. display).", help="Present content on the given screen (a.k.a. display).",
)
@click.option(
"--playback-rate",
metavar="RATE",
type=float,
default=1.0,
help="Playback rate of the video slides, see PySide6 docs for details.",
) )
@click.help_option("-h", "--help") @click.help_option("-h", "--help")
@verbosity_option @verbosity_option
@ -228,6 +235,7 @@ def present(
start_at_scene_number: int, start_at_scene_number: int,
start_at_slide_number: int, start_at_slide_number: int,
screen_number: Optional[int] = None, screen_number: Optional[int] = None,
playback_rate: float = 1.0,
) -> None: ) -> None:
""" """
Present SCENE(s), one at a time, in order. Present SCENE(s), one at a time, in order.
@ -258,13 +266,9 @@ def present(
start_at_scene_number = start_at[0] start_at_scene_number = start_at[0]
if start_at[1]: if start_at[1]:
start_at_scene_number = start_at[1] start_at_slide_number = start_at[1]
if maybe_app := QApplication.instance():
app = maybe_app
else:
app = QApplication(sys.argv)
app = qapp()
app.setApplicationName("Manim Slides") app.setApplicationName("Manim Slides")
if screen_number is not None: if screen_number is not None:
@ -291,9 +295,10 @@ def present(
presentation_index=start_at_scene_number, presentation_index=start_at_scene_number,
slide_index=start_at_slide_number, slide_index=start_at_slide_number,
screen=screen, screen=screen,
playback_rate=playback_rate,
) )
player.show() player.show()
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.exec_()) sys.exit(app.exec())

View File

@ -53,6 +53,7 @@ class Player(QMainWindow): # type: ignore[misc]
presentation_index: int = 0, presentation_index: int = 0,
slide_index: int = 0, slide_index: int = 0,
screen: Optional[QScreen] = None, screen: Optional[QScreen] = None,
playback_rate: float = 1.0,
): ):
super().__init__() super().__init__()
@ -65,11 +66,12 @@ class Player(QMainWindow): # type: ignore[misc]
self.presentation_configs = presentation_configs self.presentation_configs = presentation_configs
self.__current_presentation_index = 0 self.__current_presentation_index = 0
self.__current_slide_index = 0 self.__current_slide_index = 0
self.__current_file: Path = self.current_slide_config.file
self.current_presentation_index = presentation_index self.current_presentation_index = presentation_index
self.current_slide_index = slide_index self.current_slide_index = slide_index
self.__current_file: Path = self.current_slide_config.file
self.__playing_reversed_slide = False self.__playing_reversed_slide = False
# Widgets # Widgets
@ -100,6 +102,7 @@ class Player(QMainWindow): # type: ignore[misc]
self.media_player = QMediaPlayer(self) self.media_player = QMediaPlayer(self)
self.media_player.setVideoOutput(self.video_widget) self.media_player.setVideoOutput(self.video_widget)
self.media_player.setPlaybackRate(playback_rate)
self.presentation_changed.connect(self.presentation_changed_callback) self.presentation_changed.connect(self.presentation_changed_callback)
self.slide_changed.connect(self.slide_changed_callback) self.slide_changed.connect(self.slide_changed_callback)
@ -108,7 +111,7 @@ class Player(QMainWindow): # type: ignore[misc]
# Connecting key callbacks # Connecting key callbacks
self.config.keys.QUIT.connect(self.quit) self.config.keys.QUIT.connect(self.close)
self.config.keys.PLAY_PAUSE.connect(self.play_pause) self.config.keys.PLAY_PAUSE.connect(self.play_pause)
self.config.keys.NEXT.connect(self.next) self.config.keys.NEXT.connect(self.next)
self.config.keys.PREVIOUS.connect(self.previous) self.config.keys.PREVIOUS.connect(self.previous)
@ -161,7 +164,7 @@ class Player(QMainWindow): # type: ignore[misc]
elif -self.presentations_count <= index < 0: elif -self.presentations_count <= index < 0:
self.__current_presentation_index = index + self.presentations_count self.__current_presentation_index = index + self.presentations_count
else: else:
logger.warn(f"Could not set presentation index to {index}") logger.warn(f"Could not set presentation index to {index}.")
return return
self.presentation_changed.emit() self.presentation_changed.emit()
@ -185,7 +188,7 @@ class Player(QMainWindow): # type: ignore[misc]
elif -self.current_slides_count <= index < 0: elif -self.current_slides_count <= index < 0:
self.__current_slide_index = index + self.current_slides_count self.__current_slide_index = index + self.current_slides_count
else: else:
logger.warn(f"Could not set slide index to {index}") logger.warn(f"Could not set slide index to {index}.")
return return
self.slide_changed.emit() self.slide_changed.emit()
@ -257,7 +260,8 @@ class Player(QMainWindow): # type: ignore[misc]
self.current_presentation_index += 1 self.current_presentation_index += 1
self.current_slide_index = 0 self.current_slide_index = 0
elif self.exit_after_last_slide: elif self.exit_after_last_slide:
self.quit() self.close()
return
else: else:
logger.info("No more slide to play.") logger.info("No more slide to play.")
return return
@ -290,10 +294,9 @@ class Player(QMainWindow): # type: ignore[misc]
self.info.show() self.info.show()
@Slot() @Slot()
def quit(self) -> None: def close(self) -> None:
logger.info("Closing gracefully...") logger.info("Closing gracefully...")
self.info.deleteLater() super().close()
self.deleteLater()
@Slot() @Slot()
def next(self) -> None: def next(self) -> None:
@ -338,7 +341,7 @@ class Player(QMainWindow): # type: ignore[misc]
self.setCursor(Qt.BlankCursor) self.setCursor(Qt.BlankCursor)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.quit() self.close()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802 def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
key = event.key() key = event.key()

14
manim_slides/qt_utils.py Normal file
View File

@ -0,0 +1,14 @@
"""Qt utils."""
from PySide6.QtWidgets import QApplication
def qapp() -> QApplication:
"""
Return a QApplication instance, creating one
if needed.
"""
if app := QApplication.instance():
return app
return QApplication([])

View File

@ -45,49 +45,49 @@ class BaseSlide:
@abstractmethod @abstractmethod
def _frame_height(self) -> float: def _frame_height(self) -> float:
"""Return the scene's frame height.""" """Return the scene's frame height."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _frame_width(self) -> float: def _frame_width(self) -> float:
"""Return the scene's frame width.""" """Return the scene's frame width."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _background_color(self) -> str: def _background_color(self) -> str:
"""Return the scene's background color.""" """Return the scene's background color."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _resolution(self) -> Tuple[int, int]: def _resolution(self) -> Tuple[int, int]:
"""Return the scene's resolution used during rendering.""" """Return the scene's resolution used during rendering."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _partial_movie_files(self) -> List[Path]: def _partial_movie_files(self) -> List[Path]:
"""Return a list of partial movie files, a.k.a animations.""" """Return a list of partial movie files, a.k.a animations."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _show_progress_bar(self) -> bool: def _show_progress_bar(self) -> bool:
"""Return True if progress bar should be displayed.""" """Return True if progress bar should be displayed."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _leave_progress_bar(self) -> bool: def _leave_progress_bar(self) -> bool:
"""Return True if progress bar should be left after completed.""" """Return True if progress bar should be left after completed."""
... raise NotImplementedError
@property @property
@abstractmethod @abstractmethod
def _start_at_animation_number(self) -> Optional[int]: def _start_at_animation_number(self) -> Optional[int]:
"""If set, return the animation number at which rendering start.""" """If set, return the animation number at which rendering start."""
... raise NotImplementedError
@property @property
def canvas(self) -> MutableMapping[str, Mobject]: def canvas(self) -> MutableMapping[str, Mobject]:
@ -341,8 +341,9 @@ class BaseSlide:
) )
) )
self._pre_slide_config_kwargs = dict(loop=loop)
self._current_slide += 1 self._current_slide += 1
self._pre_slide_config_kwargs = dict(loop=loop)
self._start_animation = self._current_animation self._start_animation = self._current_animation
def _add_last_slide(self) -> None: def _add_last_slide(self) -> None:

View File

@ -7,7 +7,6 @@ import click
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QKeyEvent from PySide6.QtGui import QIcon, QKeyEvent
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication,
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QGridLayout, QGridLayout,
@ -22,6 +21,7 @@ from .commons import config_options, verbosity_option
from .config import Config, Key from .config import Config, Key
from .defaults import CONFIG_PATH from .defaults import CONFIG_PATH
from .logger import logger from .logger import logger
from .qt_utils import qapp
from .resources import * # noqa: F403 from .resources import * # noqa: F403
WINDOW_NAME: str = "Configuration Wizard" WINDOW_NAME: str = "Configuration Wizard"
@ -57,12 +57,13 @@ class Wizard(QWidget): # type: ignore
self.config = config self.config = config
self.icon = QIcon(":/icon.png") self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon) self.setWindowIcon(self.icon)
self.closed_without_saving = False
button = QDialogButtonBox.Save | QDialogButtonBox.Cancel button = QDialogButtonBox.Save | QDialogButtonBox.Cancel
self.buttonBox = QDialogButtonBox(button) self.button_box = QDialogButtonBox(button)
self.buttonBox.accepted.connect(self.save_config) self.button_box.accepted.connect(self.save_config)
self.buttonBox.rejected.connect(self.close_without_saving) self.button_box.rejected.connect(self.close_without_saving)
self.buttons = [] self.buttons = []
@ -87,17 +88,17 @@ class Wizard(QWidget): # type: ignore
) )
self.layout.addWidget(button, i, 1) self.layout.addWidget(button, i, 1)
self.layout.addWidget(self.buttonBox, len(self.buttons), 1) self.layout.addWidget(self.button_box, len(self.buttons), 1)
self.setLayout(self.layout) self.setLayout(self.layout)
def close_without_saving(self) -> None: def close_without_saving(self) -> None:
logger.debug("Closing configuration wizard without saving") logger.debug("Closing configuration wizard without saving")
self.closed_without_saving = True
self.deleteLater() self.deleteLater()
sys.exit(0)
def closeEvent(self, event: Any) -> None: # noqa: N802 def closeEvent(self, event: Any) -> None: # noqa: N802
self.closeWithoutSaving() self.close_without_saving()
event.accept() event.accept()
def save_config(self) -> None: def save_config(self) -> None:
@ -111,7 +112,7 @@ class Wizard(QWidget): # type: ignore
"Two or more actions share a common key: make sure actions have distinct key codes." "Two or more actions share a common key: make sure actions have distinct key codes."
) )
msg.setWindowTitle("Error: duplicated keys") msg.setWindowTitle("Error: duplicated keys")
msg.exec_() msg.exec()
return return
self.deleteLater() self.deleteLater()
@ -119,7 +120,7 @@ class Wizard(QWidget): # type: ignore
def open_dialog(self, button_number: int, key: Key) -> None: def open_dialog(self, button_number: int, key: Key) -> None:
button = self.buttons[button_number] button = self.buttons[button_number]
dialog = KeyInput() dialog = KeyInput()
dialog.exec_() dialog.exec()
if dialog.key is not None: if dialog.key is not None:
key_name = keymap[dialog.key] key_name = keymap[dialog.key]
key.set_ids(dialog.key) key.set_ids(dialog.key)
@ -180,12 +181,15 @@ def _init(
if config_path.exists(): if config_path.exists():
config = Config.from_file(config_path) config = Config.from_file(config_path)
app = QApplication(sys.argv) app = qapp()
app.setApplicationName("Manim Slides Wizard") app.setApplicationName("Manim Slides Wizard")
window = Wizard(config) window = Wizard(config)
window.show() window.show()
app.exec() app.exec()
if window.closed_without_saving:
sys.exit(0)
config = window.config config = window.config
if merge: if merge:

53
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]] [[package]]
name = "alabaster" name = "alabaster"
@ -1740,16 +1740,6 @@ files = [
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@ -2309,12 +2299,13 @@ files = [
[package.dependencies] [package.dependencies]
numpy = [ numpy = [
{version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\" and python_version >= \"3.8\""}, {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""},
{version = ">=1.21.2", markers = "python_version >= \"3.10\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""},
{version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""},
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, {version = ">=1.17.0", markers = "python_version >= \"3.7\""},
{version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, {version = ">=1.17.3", markers = "python_version >= \"3.8\""},
{version = ">=1.17.3", markers = "(platform_system != \"Darwin\" and platform_system != \"Linux\") and python_version >= \"3.8\" and python_version < \"3.9\" or platform_system != \"Darwin\" and python_version >= \"3.8\" and python_version < \"3.9\" and platform_machine != \"aarch64\" or platform_machine != \"arm64\" and python_version >= \"3.8\" and python_version < \"3.9\" and platform_system != \"Linux\" or (platform_machine != \"arm64\" and platform_machine != \"aarch64\") and python_version >= \"3.8\" and python_version < \"3.9\""},
] ]
[[package]] [[package]]
@ -3053,6 +3044,24 @@ pytest = ">=7.3.1"
[package.extras] [package.extras]
test = ["coverage (>=7.2.7)", "pytest-mock (>=3.10)"] test = ["coverage (>=7.2.7)", "pytest-mock (>=3.10)"]
[[package]]
name = "pytest-qt"
version = "4.2.0"
description = "pytest support for PyQt and PySide applications"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-qt-4.2.0.tar.gz", hash = "sha256:00a17b586dd530b6d7a9399923a40489ca4a9a309719011175f55dc6b5dc8f41"},
{file = "pytest_qt-4.2.0-py2.py3-none-any.whl", hash = "sha256:a7659960a1ab2af8fc944655a157ff45d714b80ed7a6af96a4b5bb99ecf40a22"},
]
[package.dependencies]
pytest = ">=3.0.0"
[package.extras]
dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"]
[[package]] [[package]]
name = "pytest-xdist" name = "pytest-xdist"
version = "3.3.1" version = "3.3.1"
@ -3149,7 +3158,6 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@ -3157,15 +3165,8 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@ -3182,7 +3183,6 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@ -3190,7 +3190,6 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@ -4237,4 +4236,4 @@ sphinx-directive = ["docutils", "manim"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.8.1,<3.12" python-versions = ">=3.8.1,<3.12"
content-hash = "d691d1abda28f4b940f44bb689b9c34220734168f45f66b905e8b3323ad51e36" content-hash = "a973a459c920323757e0a99c9ec100e9a1f12088deacea718fdd74f3fc8cf357"

View File

@ -104,6 +104,7 @@ manimgl = "^1.6.1"
pytest = "^7.4.0" pytest = "^7.4.0"
pytest-cov = "^4.1.0" pytest-cov = "^4.1.0"
pytest-env = "^0.8.2" pytest-env = "^0.8.2"
pytest-qt = "^4.2.0"
pytest-xdist = "^3.3.1" pytest-xdist = "^3.3.1"
[tool.poetry.plugins] [tool.poetry.plugins]

View File

@ -8,32 +8,32 @@ import pytest
from manim_slides.config import PresentationConfig from manim_slides.config import PresentationConfig
@pytest.fixture @pytest.fixture(scope="session")
def tests_folder() -> Iterator[Path]: def tests_folder() -> Iterator[Path]:
yield Path(__file__).parent.resolve(strict=True) yield Path(__file__).parent.resolve(strict=True)
@pytest.fixture @pytest.fixture(scope="session")
def project_folder(tests_folder: Path) -> Iterator[Path]: def project_folder(tests_folder: Path) -> Iterator[Path]:
yield tests_folder.parent.resolve(strict=True) yield tests_folder.parent.resolve(strict=True)
@pytest.fixture @pytest.fixture(scope="session")
def data_folder(tests_folder: Path) -> Iterator[Path]: def data_folder(tests_folder: Path) -> Iterator[Path]:
yield (tests_folder / "data").resolve(strict=True) yield (tests_folder / "data").resolve(strict=True)
@pytest.fixture @pytest.fixture(scope="session")
def slides_folder(data_folder: Path) -> Iterator[Path]: def slides_folder(data_folder: Path) -> Iterator[Path]:
yield (data_folder / "slides").resolve(strict=True) yield (data_folder / "slides").resolve(strict=True)
@pytest.fixture @pytest.fixture(scope="session")
def slides_file(data_folder: Path) -> Iterator[Path]: def slides_file(data_folder: Path) -> Iterator[Path]:
yield (data_folder / "slides.py").resolve(strict=True) yield (data_folder / "slides.py").resolve(strict=True)
@pytest.fixture @pytest.fixture(scope="session")
def manimgl_config(project_folder: Path) -> Iterator[Path]: def manimgl_config(project_folder: Path) -> Iterator[Path]:
yield (project_folder / "custom_config.yml").resolve(strict=True) yield (project_folder / "custom_config.yml").resolve(strict=True)
@ -61,7 +61,7 @@ def paths() -> Generator[List[Path], None, None]:
yield [random_path() for _ in range(20)] yield [random_path() for _ in range(20)]
@pytest.fixture @pytest.fixture(scope="session")
def presentation_config( def presentation_config(
slides_folder: Path, slides_folder: Path,
) -> Generator[PresentationConfig, None, None]: ) -> Generator[PresentationConfig, None, None]:

109
tests/test_base_slide.py Normal file
View File

@ -0,0 +1,109 @@
from typing import MutableMapping
import pytest
from manim_slides.defaults import FFMPEG_BIN
from manim_slides.slide.base import BaseSlide
@pytest.fixture
def base_slide() -> BaseSlide:
return BaseSlide() # type: ignore[abstract]
class TestBaseSlide:
def test_ffmpeg_bin(self, base_slide: BaseSlide) -> None:
assert base_slide._ffmpeg_bin == FFMPEG_BIN
def test_frame_height(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._frame_height
def test_frame_width(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._frame_width
def test_background_color(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._background_color
def test_resolution(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._resolution
def test_partial_movie_files(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._partial_movie_files
def test_show_progress_bar(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._show_progress_bar
def test_leave_progress_bar(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._leave_progress_bar
def test_start_at_animation_number(self, base_slide: BaseSlide) -> None:
with pytest.raises(NotImplementedError):
_ = base_slide._start_at_animation_number
def test_canvas(self, base_slide: BaseSlide) -> None:
assert len(base_slide.canvas) == 0
assert isinstance(base_slide.canvas, MutableMapping)
def test_add_to__and_remove_from_canvas(self, base_slide: BaseSlide) -> None:
assert len(base_slide.canvas) == 0
base_slide.add_to_canvas(a=1, b=2)
assert len(base_slide.canvas) == 2
assert base_slide.canvas["a"] == 1
assert base_slide.canvas["b"] == 2
with pytest.raises(KeyError):
_ = base_slide.canvas["c"]
base_slide.add_to_canvas(b=3, c=4)
assert len(base_slide.canvas) == 3
assert sorted(base_slide.canvas_mobjects) == [1, 3, 4]
base_slide.remove_from_canvas("a", "b", "c")
assert len(base_slide.canvas) == 0
with pytest.raises(KeyError):
base_slide.remove_from_canvas("a")
def test_mobjects_without_canvas(self) -> None:
pass # This property should be tested in test_slide.py
def test_wait_time_between_slides(self, base_slide: BaseSlide) -> None:
assert base_slide.wait_time_between_slides == 0.0
base_slide.wait_time_between_slides = 1.0
assert base_slide.wait_time_between_slides == 1.0
base_slide.wait_time_between_slides = -1.0
assert base_slide.wait_time_between_slides == 0.0
def test_play(self) -> None:
pass # This method should be tested in test_slide.py
def test_next_slide(self) -> None:
pass # This method should be tested in test_slide.py
def test_add_last_slide(self) -> None:
pass # This method should be tested in test_slide.py
def test_save_slides(self) -> None:
pass # This method should be tested in test_slide.py
def test_zoom(self) -> None:
pass # This method should be tested in test_slide.py
def test_wipe(self) -> None:
pass # This method should be tested in test_slide.py

View File

@ -1,3 +1,4 @@
import shutil
from pathlib import Path from pathlib import Path
from manim_slides.defaults import CONFIG_PATH, FFMPEG_BIN, FOLDER_PATH from manim_slides.defaults import CONFIG_PATH, FFMPEG_BIN, FOLDER_PATH
@ -13,3 +14,9 @@ def test_config_path() -> None:
def test_ffmpeg_bin() -> None: def test_ffmpeg_bin() -> None:
assert FFMPEG_BIN == Path("ffmpeg") assert FFMPEG_BIN == Path("ffmpeg")
def test_ffmpeg_bin_exists() -> None:
assert (
shutil.which(str(FFMPEG_BIN)) is not None
), "If this fails, many other tests will fail"

View File

@ -8,6 +8,8 @@ from manim_slides.__main__ import cli
def test_help() -> None: def test_help() -> None:
runner = CliRunner() runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(cli, ["-S", "--help"]) results = runner.invoke(cli, ["-S", "--help"])
assert results.exit_code == 0 assert results.exit_code == 0
@ -15,28 +17,30 @@ def test_help() -> None:
results = runner.invoke(cli, ["-S", "-h"]) results = runner.invoke(cli, ["-S", "-h"])
assert results.exit_code == 0 assert results.exit_code == 0
assert "Usage: cli [OPTIONS] COMMAND [ARGS]..." in results.stdout
def test_defaults_to_present(slides_folder: Path) -> None: def test_defaults_to_present(slides_folder: Path) -> None:
runner = CliRunner() runner = CliRunner()
with runner.isolated_filesystem(): with runner.isolated_filesystem():
results = runner.invoke( results = runner.invoke(cli, ["-S", "BasicSlide", "--help"])
cli, ["BasicSlide", "--folder", str(slides_folder), "-s"]
)
assert results.exit_code == 0 assert results.exit_code == 0
assert "Usage: cli present" in results.stdout
def test_present(slides_folder: Path) -> None: @pytest.mark.parametrize(
["subcommand"], [["present"], ["convert"], ["init"], ["list-scenes"], ["wizard"]]
)
def test_help_subcommand(subcommand: str) -> None:
runner = CliRunner() runner = CliRunner()
with runner.isolated_filesystem(): with runner.isolated_filesystem():
results = runner.invoke( results = runner.invoke(cli, ["-S", subcommand, "--help"])
cli, ["present", "BasicSlide", "--folder", str(slides_folder), "-s"]
)
assert results.exit_code == 0 assert results.exit_code == 0
assert f"Usage: cli {subcommand}" in results.stdout
@pytest.mark.parametrize(("extension",), [("html",), ("pdf",), ("pptx",)]) @pytest.mark.parametrize(("extension",), [("html",), ("pdf",), ("pptx",)])
@ -90,8 +94,3 @@ def test_list_scenes(slides_folder: Path) -> None:
assert results.exit_code == 0 assert results.exit_code == 0
assert "BasicSlide" in results.output assert "BasicSlide" in results.output
def test_wizard() -> None:
# TODO
pass

1
tests/test_player.py Normal file
View File

@ -0,0 +1 @@
# TODO

141
tests/test_present.py Normal file
View File

@ -0,0 +1,141 @@
from pathlib import Path
from typing import Iterator, Tuple
import pytest
from click.testing import CliRunner
from PySide6.QtWidgets import QApplication
from manim_slides.present import present
@pytest.fixture(autouse=True)
def auto_shutdown_qapp() -> Iterator[None]:
if app := QApplication.instance():
app.shutdown()
yield
if app := QApplication.instance():
app.shutdown()
@pytest.fixture(scope="session")
def args(slides_folder: Path) -> Iterator[Tuple[str, ...]]:
yield ("--folder", str(slides_folder), "--skip-all", "--playback-rate", "25")
def test_present(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", *args])
assert results.exit_code == 0
assert results.stdout == ""
def test_present_unexisting_slide(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["UnexistingSlide", *args])
assert results.exit_code != 0
assert "UnexistingSlide.json does not exist" in results.stdout
def test_present_full_screen(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", "--fullscreen", *args])
assert results.exit_code == 0
assert results.stdout == ""
def test_present_hide_mouse(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", "--hide-mouse", *args])
assert results.exit_code == 0
assert results.stdout == ""
def test_present_ignore_aspect_ratio(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
present, ["BasicSlide", "--aspect-ratio", "ignore", *args]
)
assert results.exit_code == 0
assert results.stdout == ""
def test_present_start_at(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", "--start-at", "-1,-1", *args])
assert results.exit_code == 0
assert results.stdout == ""
def test_present_start_at_invalid(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", "--start-at", "0,1234", *args])
assert results.exit_code == 0
assert "Could not set presentation index to 1234"
def test_present_start_at_scene_number(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
present, ["BasicSlide", "BasicSlide", "--start-at-scene-number", "1", *args]
)
assert results.exit_code == 0
assert results.stdout == ""
def test_present_start_at_slide_number(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(
present, ["BasicSlide", "--start-at-slide-number", "1", *args]
)
assert results.exit_code == 0
assert results.stdout == ""
def test_present_set_screen(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", "--screen", "0", *args])
assert results.exit_code == 0
assert results.stdout == ""
@pytest.mark.skip(reason="Fails when running the whole test suite.")
def test_present_set_invalid_screen(args: Tuple[str, ...]) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
results = runner.invoke(present, ["BasicSlide", "--screen", "999", *args])
assert results.exit_code == 0
assert "Invalid screen number 999" in results.stdout

14
tests/test_qt_utils.py Normal file
View File

@ -0,0 +1,14 @@
from PySide6.QtWidgets import QApplication
from manim_slides.qt_utils import qapp
def test_qapp() -> None:
assert isinstance(qapp(), QApplication)
def test_duplicated_qapp() -> None:
app1 = qapp()
app2 = qapp()
assert app1 == app2

View File

@ -197,6 +197,42 @@ class TestSlide:
assert text not in self.mobjects assert text not in self.mobjects
assert bye 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()
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
@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
@assert_constructs @assert_constructs
class TestCanvas(Slide): class TestCanvas(Slide):
def construct(self) -> None: def construct(self) -> None:

181
tests/test_wizard.py Normal file
View File

@ -0,0 +1,181 @@
from pathlib import Path
from click.testing import CliRunner
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QApplication,
QMessageBox,
)
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from manim_slides.config import Config, Key
from manim_slides.defaults import CONFIG_PATH
from manim_slides.wizard import KeyInput, Wizard, init, wizard
class TestKeyInput:
def test_default_is_none(self, qtbot: QtBot) -> None:
widget = KeyInput()
widget.show()
qtbot.addWidget(widget)
assert widget.key is None
def test_send_key(self, qtbot: QtBot) -> None:
widget = KeyInput()
widget.show()
qtbot.addWidget(widget)
qtbot.keyPress(widget, Qt.Key_Q)
assert widget.key is Qt.Key_Q.value
class TestWizard:
def test_close_without_saving(self, qtbot: QtBot) -> None:
wizard = Wizard(Config())
wizard.show()
qtbot.addWidget(wizard)
wizard.button_box.rejected.emit()
assert wizard.closed_without_saving
def test_save_valid_config(self, qtbot: QtBot) -> None:
widget = Wizard(Config())
widget.show()
qtbot.addWidget(widget)
widget.button_box.accepted.emit()
assert not widget.closed_without_saving
def test_save_invalid_config(self, qtbot: QtBot, monkeypatch: MonkeyPatch) -> None:
wizard = Wizard(Config())
wizard.show()
qtbot.addWidget(wizard)
def open_dialog(button_number: int, key: Key) -> None:
button = wizard.buttons[button_number]
dialog = KeyInput()
qtbot.addWidget(dialog)
qtbot.keyPress(dialog, Qt.Key_Q)
assert dialog.key is not None
key.set_ids(dialog.key)
button.setText("Q")
assert button.text() == "Q"
dialog.close()
message_boxes = []
def exec_patched(self: QMessageBox) -> None:
self.show()
message_boxes.append(self)
monkeypatch.setattr(QMessageBox, "exec", exec_patched)
for i, (key, _) in enumerate(wizard.config.keys.dict().items()):
open_dialog(i, getattr(wizard.config.keys, key))
wizard.button_box.accepted.emit()
message_box = message_boxes.pop()
qtbot.addWidget(message_box)
assert message_box.isVisible()
def test_init() -> None:
runner = CliRunner()
with runner.isolated_filesystem():
assert not CONFIG_PATH.exists()
results = runner.invoke(
init,
)
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
def test_init_custom_path() -> None:
runner = CliRunner()
custom_path = Path("config.toml")
with runner.isolated_filesystem():
assert not custom_path.exists()
results = runner.invoke(
init,
["--config", str(custom_path)],
)
assert results.exit_code == 0
assert not CONFIG_PATH.exists()
assert custom_path.exists()
assert Config().dict() == Config.from_file(custom_path).dict()
def test_init_path_exists() -> None:
runner = CliRunner()
with runner.isolated_filesystem():
assert not CONFIG_PATH.exists()
results = runner.invoke(
init,
)
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
results = runner.invoke(init, input="o")
assert results.exit_code == 0
results = runner.invoke(init, input="m")
assert results.exit_code == 0
results = runner.invoke(init, input="q")
assert results.exit_code == 0
def test_wizard(monkeypatch: MonkeyPatch) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
assert not CONFIG_PATH.exists()
def show(self: Wizard) -> None:
self.button_box.accepted.emit()
def exec_patched(self: QApplication) -> None:
pass
monkeypatch.setattr(Wizard, "show", show)
monkeypatch.setattr(QApplication, "exec", exec_patched)
results = runner.invoke(
wizard,
)
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
def test_wizard_closed_without_saving(monkeypatch: MonkeyPatch) -> None:
runner = CliRunner()
with runner.isolated_filesystem():
assert not CONFIG_PATH.exists()
def show(self: Wizard) -> None:
self.button_box.rejected.emit()
def exec_patched(self: QApplication) -> None:
pass
monkeypatch.setattr(Wizard, "show", show)
monkeypatch.setattr(QApplication, "exec", exec_patched)
results = runner.invoke(
wizard,
)
assert results.exit_code == 0
assert not CONFIG_PATH.exists()