textual-paint/tests/test_snapshots.py

318 lines
20 KiB
Python
Raw Normal View History

2023-09-13 04:22:22 +03:00
"""Visual regression tests, using pytest-textual-snapshot. Run with `pytest`."""
2023-09-09 06:38:37 +03:00
from pathlib import Path, PurePath
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
from typing import (TYPE_CHECKING, Awaitable, Callable, Generator, Iterable,
Protocol)
2023-09-07 22:14:07 +03:00
import pytest
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
from pyfakefs.fake_filesystem import FakeFilesystem
2023-09-09 08:12:21 +03:00
from textual.geometry import Offset
2023-09-09 02:59:36 +03:00
from textual.pilot import Pilot
2023-09-08 22:03:50 +03:00
from textual.widgets import Input
2023-09-07 22:14:07 +03:00
from tests.pilot_helpers import click_by_attr, click_by_index, drag
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
from textual_paint.figlet_font_writer import FIGletFontWriter
2023-09-12 02:28:33 +03:00
if TYPE_CHECKING:
# When tests are run, paint.py is re-evaluated,
# leading to a different class of the same name at runtime.
from textual_paint.paint import PaintApp
2023-09-09 02:59:36 +03:00
2023-09-09 06:38:37 +03:00
class SnapCompareType(Protocol):
"""Type of the function returned by the snap_compare fixture."""
def __call__(
self,
app_path: str | PurePath,
press: Iterable[str] = (),
terminal_size: tuple[int, int] = (80, 24),
run_before: Callable[[Pilot], Awaitable[None] | None] | None = None, # type: ignore
) -> bool:
...
2023-09-07 22:14:07 +03:00
# These paths are treated as relative to this file.
APPS_DIR = Path("../src/textual_paint")
PAINT = APPS_DIR / "paint.py"
GALLERY = APPS_DIR / "gallery.py"
2023-09-08 00:43:14 +03:00
LARGER = (81, 38)
2023-09-09 04:40:29 +03:00
"""Large enough to show the Textual Paint app's main UI and most dialogs comfortably."""
2023-09-09 02:59:36 +03:00
LARGEST = (107, 42)
"""Large enough to show the Edit Colors dialog, which is a bit oversized."""
2023-09-08 00:43:14 +03:00
2023-09-09 02:59:36 +03:00
# Prevent flaky tests due to timing issues.
Input.cursor_blink = False # type: ignore
@pytest.fixture(params=[
{"theme": "light", "ascii_only": False},
{"theme": "dark", "ascii_only": False},
{"theme": "light", "ascii_only": True},
{"theme": "dark", "ascii_only": True},
], ids=lambda param: f"{param['theme']}_{'ascii' if param['ascii_only'] else 'unicode'}")
2023-09-09 06:38:37 +03:00
def each_theme(request: pytest.FixtureRequest):
2023-09-09 05:53:58 +03:00
"""Fixture to test each combination of UI styles."""
theme = request.param.get("theme")
ascii_only = request.param.get("ascii_only")
from textual_paint.args import args
args.theme = theme
args.ascii_only = ascii_only
2023-09-13 04:22:22 +03:00
yield # run the test
args.theme = "light"
args.ascii_only = False
2023-09-13 01:19:40 +03:00
REPO_DIR_ABSOLUTE = Path(__file__).parent.parent.resolve()
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
@pytest.fixture
def my_fs(fs: FakeFilesystem) -> Generator[FakeFilesystem, None, None]:
2023-09-13 04:22:22 +03:00
"""Fixture to fake the filesystem, except for the repo directory."""
2023-09-13 01:19:40 +03:00
2023-09-13 04:22:22 +03:00
# Without the repo dir, textual paint will fail to load FIGlet fonts or dialog icons.
# Without the __snapshots__ dir, pytest-textual-snapshot will show "No history for this test" in the report.
2023-09-13 01:19:40 +03:00
print("adding real directory", REPO_DIR_ABSOLUTE)
fs.add_real_directory(REPO_DIR_ABSOLUTE)
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
Fix DirectoryTree retaining real pathlib access This fixes the following error: ----------------------------------------------- Captured stderr call ----------------------------------------------- ╭───────────────────── Traceback (most recent call last) ──────────────────────╮ │ /home/io/Projects/textual-paint/src/textual_paint/file_dialogs.py:86 in │ │ on_mount │ │ │ │ 83 │ def on_mount(self) -> None: │ │ 84 │ │ """Called when the window is mounted.""" │ │ 85 │ │ self.content.mount( │ │ ❱ 86 │ │ │ EnhancedDirectoryTree(path="/"), │ │ 87 │ │ │ Horizontal( │ │ 88 │ │ │ │ Label(_("File name:")), │ │ 89 │ │ │ │ Input(classes="filename_input autofocus", value=self._ │ │ │ │ ╭──────────────────── locals ────────────────────╮ │ │ │ self = OpenDialogWindow(id='window_auto_id_0') │ │ │ ╰────────────────────────────────────────────────╯ │ │ │ │ in __init__:119 │ │ │ │ 116 │ │ self._load_queue: Queue[TreeNode[DirEntry]] = Queue() │ │ 117 │ │ super().__init__( │ │ 118 │ │ │ str(path), │ │ ❱ 119 │ │ │ data=DirEntry(self.PATH(path)), │ │ 120 │ │ │ name=name, │ │ 121 │ │ │ id=id, │ │ 122 │ │ │ classes=classes, │ │ │ │ ╭───────────────── locals ──────────────────╮ │ │ │ classes = None │ │ │ │ disabled = False │ │ │ │ id = None │ │ │ │ name = None │ │ │ │ path = '/' │ │ │ │ self = EnhancedDirectoryTree( │ │ │ │ │ id='some fake shit', │ │ │ │ │ name='some more fake shit' │ │ │ │ ) │ │ │ ╰───────────────────────────────────────────╯ │ │ │ │ in __new__:960 │ │ │ │ 957 │ def __new__(cls, *args, **kwargs): │ │ 958 │ │ if cls is Path: │ │ 959 │ │ │ cls = WindowsPath if os.name == 'nt' else PosixPath │ │ ❱ 960 │ │ self = cls._from_parts(args) │ │ 961 │ │ if not self._flavour.is_supported: │ │ 962 │ │ │ raise NotImplementedError("cannot instantiate %r on your │ │ 963 │ │ │ │ │ │ │ │ │ % (cls.__name__,)) │ │ │ │ ╭──────────── locals ─────────────╮ │ │ │ args = ('/',) │ │ │ │ cls = <class 'pathlib.Path'> │ │ │ │ kwargs = {} │ │ │ ╰─────────────────────────────────╯ │ │ │ │ in _from_parts:594 │ │ │ │ 591 │ │ # We need to call _parse_args on the instance, so as to get t │ │ 592 │ │ # right flavour. │ │ 593 │ │ self = object.__new__(cls) │ │ ❱ 594 │ │ drv, root, parts = self._parse_args(args) │ │ 595 │ │ self._drv = drv │ │ 596 │ │ self._root = root │ │ 597 │ │ self._parts = parts │ │ │ │ ╭──────────────────────────── locals ─────────────────────────────╮ │ │ │ args = ('/',) │ │ │ │ cls = <class 'pathlib.Path'> │ │ │ │ self = <repr-error "'Path' object has no attribute '_flavour'"> │ │ │ ╰─────────────────────────────────────────────────────────────────╯ │ │ │ │ in _parse_args:587 │ │ │ │ 584 │ │ │ │ │ │ "argument should be a str object or an os.Pat │ │ 585 │ │ │ │ │ │ "object returning str, not %r" │ │ 586 │ │ │ │ │ │ % type(a)) │ │ ❱ 587 │ │ return cls._flavour.parse_parts(parts) │ │ 588 │ │ │ 589 │ @classmethod │ │ 590 │ def _from_parts(cls, args): │ │ │ │ ╭──────────── locals ────────────╮ │ │ │ a = '/' │ │ │ │ args = ('/',) │ │ │ │ cls = <class 'pathlib.Path'> │ │ │ │ parts = ['/'] │ │ │ ╰────────────────────────────────╯ │ ╰──────────────────────────────────────────────────────────────────────────────╯ AttributeError: type object 'Path' has no attribute '_flavour' ...which was itself masked by an error: /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/_doc.py:136: in take_svg_screenshot app.run( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1207: in run asyncio.run(run_app()) /usr/lib/python3.10/asyncio/runners.py:44: in run return loop.run_until_complete(main) /usr/lib/python3.10/asyncio/base_events.py:649: in run_until_complete return future.result() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1196: in run_app await self.run_async( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1168: in run_async await app._shutdown() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:2186: in _shutdown await self._close_all() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:2166: in _close_all await self._prune_node(stack_screen) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:2656: in _prune_node await asyncio.gather(*close_messages) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/message_pump.py:453: in _close_messages await self._task /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/message_pump.py:469: in _process_messages await self._pre_process() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/message_pump.py:490: in _pre_process self.app._handle_exception(error) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1822: in _handle_exception self._fatal_error() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1827: in _fatal_error traceback = Traceback( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/traceback.py:264: in __init__ trace = self.extract( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/traceback.py:449: in extract locals={ /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/traceback.py:450: in <dictcomp> key: pretty.traverse( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/pretty.py:852: in traverse node = _traverse(_object, root=True) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/pretty.py:647: in _traverse args = list(iter_rich_args(rich_repr_result)) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/pretty.py:614: in iter_rich_args for arg in rich_args: /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/widget.py:2516: in __rich_repr__ yield "id", self.id, None _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <[AttributeError("'EnhancedDirectoryTree' object has no attribute '_id'") raised in repr()] EnhancedDirectoryTree object at 0x7f06b38c89d0> @property def id(self) -> str | None: """The ID of this node, or None if the node has no ID.""" > return self._id E AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/dom.py:483: AttributeError ...which I worked around by adding class vars to EnhancedDirectoryTree: _id = "some fake shit" _name = "some more fake shit" It was also a heisenbug. I was able to run the tests, even update snapshots, running in debug mode in VS Code.
2023-09-13 02:20:36 +03:00
# DirectoryTree stores a reference to Path for some reason, making my life more difficult.
from textual.widgets._directory_tree import DirectoryTree
orig_PATH = DirectoryTree.PATH
DirectoryTree.PATH = Path
2023-09-13 04:22:22 +03:00
# TODO: use proper(?) mocking or figure out how to get FigletFont to find the real font files.
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
# This folder doesn't actually exist on my system, so it's not getting them from there.
# from pyfiglet import SHARED_DIRECTORY
# fs.add_real_directory(SHARED_DIRECTORY)
# Don't fail trying to load the default font "standard", we don't need it!
# `pkg_resources` doesn't seem to work with pyfakefs.
from pyfiglet import FigletFont
def preloadFont(self: FigletFont, font: str):
dumb_font = FIGletFontWriter(commentLines=["Stupid font for testing"])
for ordinal in dumb_font.charOrder:
dumb_font.figChars[ordinal] = "fallback font for testing"
return dumb_font.createFigFileData()
FigletFont.preloadFont = preloadFont
# Add an extra file to show how a file looks in the EnhancedDirectoryTree widget.
fs.create_file("/pyfakefs_added_file.txt", contents="pyfakefs ate ur FS")
yield fs
Fix DirectoryTree retaining real pathlib access This fixes the following error: ----------------------------------------------- Captured stderr call ----------------------------------------------- ╭───────────────────── Traceback (most recent call last) ──────────────────────╮ │ /home/io/Projects/textual-paint/src/textual_paint/file_dialogs.py:86 in │ │ on_mount │ │ │ │ 83 │ def on_mount(self) -> None: │ │ 84 │ │ """Called when the window is mounted.""" │ │ 85 │ │ self.content.mount( │ │ ❱ 86 │ │ │ EnhancedDirectoryTree(path="/"), │ │ 87 │ │ │ Horizontal( │ │ 88 │ │ │ │ Label(_("File name:")), │ │ 89 │ │ │ │ Input(classes="filename_input autofocus", value=self._ │ │ │ │ ╭──────────────────── locals ────────────────────╮ │ │ │ self = OpenDialogWindow(id='window_auto_id_0') │ │ │ ╰────────────────────────────────────────────────╯ │ │ │ │ in __init__:119 │ │ │ │ 116 │ │ self._load_queue: Queue[TreeNode[DirEntry]] = Queue() │ │ 117 │ │ super().__init__( │ │ 118 │ │ │ str(path), │ │ ❱ 119 │ │ │ data=DirEntry(self.PATH(path)), │ │ 120 │ │ │ name=name, │ │ 121 │ │ │ id=id, │ │ 122 │ │ │ classes=classes, │ │ │ │ ╭───────────────── locals ──────────────────╮ │ │ │ classes = None │ │ │ │ disabled = False │ │ │ │ id = None │ │ │ │ name = None │ │ │ │ path = '/' │ │ │ │ self = EnhancedDirectoryTree( │ │ │ │ │ id='some fake shit', │ │ │ │ │ name='some more fake shit' │ │ │ │ ) │ │ │ ╰───────────────────────────────────────────╯ │ │ │ │ in __new__:960 │ │ │ │ 957 │ def __new__(cls, *args, **kwargs): │ │ 958 │ │ if cls is Path: │ │ 959 │ │ │ cls = WindowsPath if os.name == 'nt' else PosixPath │ │ ❱ 960 │ │ self = cls._from_parts(args) │ │ 961 │ │ if not self._flavour.is_supported: │ │ 962 │ │ │ raise NotImplementedError("cannot instantiate %r on your │ │ 963 │ │ │ │ │ │ │ │ │ % (cls.__name__,)) │ │ │ │ ╭──────────── locals ─────────────╮ │ │ │ args = ('/',) │ │ │ │ cls = <class 'pathlib.Path'> │ │ │ │ kwargs = {} │ │ │ ╰─────────────────────────────────╯ │ │ │ │ in _from_parts:594 │ │ │ │ 591 │ │ # We need to call _parse_args on the instance, so as to get t │ │ 592 │ │ # right flavour. │ │ 593 │ │ self = object.__new__(cls) │ │ ❱ 594 │ │ drv, root, parts = self._parse_args(args) │ │ 595 │ │ self._drv = drv │ │ 596 │ │ self._root = root │ │ 597 │ │ self._parts = parts │ │ │ │ ╭──────────────────────────── locals ─────────────────────────────╮ │ │ │ args = ('/',) │ │ │ │ cls = <class 'pathlib.Path'> │ │ │ │ self = <repr-error "'Path' object has no attribute '_flavour'"> │ │ │ ╰─────────────────────────────────────────────────────────────────╯ │ │ │ │ in _parse_args:587 │ │ │ │ 584 │ │ │ │ │ │ "argument should be a str object or an os.Pat │ │ 585 │ │ │ │ │ │ "object returning str, not %r" │ │ 586 │ │ │ │ │ │ % type(a)) │ │ ❱ 587 │ │ return cls._flavour.parse_parts(parts) │ │ 588 │ │ │ 589 │ @classmethod │ │ 590 │ def _from_parts(cls, args): │ │ │ │ ╭──────────── locals ────────────╮ │ │ │ a = '/' │ │ │ │ args = ('/',) │ │ │ │ cls = <class 'pathlib.Path'> │ │ │ │ parts = ['/'] │ │ │ ╰────────────────────────────────╯ │ ╰──────────────────────────────────────────────────────────────────────────────╯ AttributeError: type object 'Path' has no attribute '_flavour' ...which was itself masked by an error: /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/_doc.py:136: in take_svg_screenshot app.run( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1207: in run asyncio.run(run_app()) /usr/lib/python3.10/asyncio/runners.py:44: in run return loop.run_until_complete(main) /usr/lib/python3.10/asyncio/base_events.py:649: in run_until_complete return future.result() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1196: in run_app await self.run_async( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1168: in run_async await app._shutdown() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:2186: in _shutdown await self._close_all() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:2166: in _close_all await self._prune_node(stack_screen) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:2656: in _prune_node await asyncio.gather(*close_messages) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/message_pump.py:453: in _close_messages await self._task /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/message_pump.py:469: in _process_messages await self._pre_process() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/message_pump.py:490: in _pre_process self.app._handle_exception(error) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1822: in _handle_exception self._fatal_error() /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/app.py:1827: in _fatal_error traceback = Traceback( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/traceback.py:264: in __init__ trace = self.extract( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/traceback.py:449: in extract locals={ /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/traceback.py:450: in <dictcomp> key: pretty.traverse( /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/pretty.py:852: in traverse node = _traverse(_object, root=True) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/pretty.py:647: in _traverse args = list(iter_rich_args(rich_repr_result)) /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/rich/pretty.py:614: in iter_rich_args for arg in rich_args: /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/widget.py:2516: in __rich_repr__ yield "id", self.id, None _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <[AttributeError("'EnhancedDirectoryTree' object has no attribute '_id'") raised in repr()] EnhancedDirectoryTree object at 0x7f06b38c89d0> @property def id(self) -> str | None: """The ID of this node, or None if the node has no ID.""" > return self._id E AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? /home/io/Projects/textual-paint/.venv/lib/python3.10/site-packages/textual/dom.py:483: AttributeError ...which I worked around by adding class vars to EnhancedDirectoryTree: _id = "some fake shit" _name = "some more fake shit" It was also a heisenbug. I was able to run the tests, even update snapshots, running in debug mode in VS Code.
2023-09-13 02:20:36 +03:00
# probably don't need to actually clean up, but whatever
DirectoryTree.PATH = orig_PATH
2023-09-09 06:38:37 +03:00
def test_paint_app(snap_compare: SnapCompareType, each_theme: None):
2023-09-08 00:43:14 +03:00
assert snap_compare(PAINT, terminal_size=LARGER)
2023-09-07 22:14:07 +03:00
2023-09-09 06:38:37 +03:00
def test_paint_stretch_skew_dialog(snap_compare: SnapCompareType, each_theme: None):
2023-09-07 22:14:07 +03:00
assert snap_compare(PAINT, press=["ctrl+w"])
2023-09-09 06:38:37 +03:00
def test_paint_flip_rotate_dialog(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+r"])
2023-09-09 06:38:37 +03:00
def test_paint_image_attributes_dialog(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+e"])
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
def test_paint_open_dialog(snap_compare: SnapCompareType, each_theme: None, my_fs: None):
2023-09-08 00:43:14 +03:00
assert snap_compare(PAINT, press=["ctrl+o"], terminal_size=LARGER)
Use pyfakefs for file dialog tests - The app's directory structure is not a constant, and shouldn't play into this test. Aside from codebase restructuring, directories like `__pycache__` can come and go. - Even if a temporary directory were created with files enough to fill the view, the scrollbar would still change based on the folder structure outside of the temporary folder. - pyfakefs is one way to ensure a consistent view of a folder structure for testing. It allows adding real folders in a readonly way. It's more complicated than I thought it would be going in, since I had to add workarounds for pyfiglet and pytest-textual-snapshot, and handle an edge case in my EnhancedDirectoryTree (which got an error which seems to be swallowed?), not to mention pyfakefs raises an error saying "No such file or directory in the fake filesystem" when actually it's the real directory not existing when trying to add it to the fake filesystem, and VS Code was hiding stack frames and refusing to step into library code, and it turned out that I was resolving the absolute path wrong, but it looked right to me because the only part that was missing was "textual-paint", when, at a glance it seemed present, since the "textual_paint" part was present. Ay-ay-ay! - I don't know if this will fix the problem I saw where these tests' snapshots all changed with no visual or even structural changes, just the IDs of elements changing. I don't know what caused that. Oh yeah and this is still actually a problem: ============================================= short test summary info ============================================== FAILED tests/test_snapshots.py::test_paint_open_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_open_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_unicode] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[light_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? FAILED tests/test_snapshots.py::test_paint_save_dialog[dark_ascii] - AttributeError: 'EnhancedDirectoryTree' object has no attribute '_id'. Did you mean: 'id'? ========================== 8 failed, 56 passed, 1 xfailed, 1 warning in 152.86s (0:02:32) ========================== It worked when running in debug, but not when running normally.
2023-09-13 01:38:08 +03:00
def test_paint_save_dialog(snap_compare: SnapCompareType, each_theme: None, my_fs: None):
2023-09-08 00:43:14 +03:00
assert snap_compare(PAINT, press=["ctrl+s"], terminal_size=LARGER)
2023-09-09 06:38:37 +03:00
def test_paint_help_dialog(snap_compare: SnapCompareType, each_theme: None):
2023-09-08 00:43:14 +03:00
assert snap_compare(PAINT, press=["f1"], terminal_size=LARGER)
2023-09-09 06:38:37 +03:00
def test_paint_view_bitmap(snap_compare: SnapCompareType):
assert snap_compare(PAINT, press=["ctrl+f"])
2023-09-09 06:38:37 +03:00
def test_paint_invert_and_exit(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+i", "ctrl+q"])
2023-09-09 06:38:37 +03:00
def test_swap_selected_colors(snap_compare: SnapCompareType):
async def swap_selected_colors(pilot: Pilot[None]):
2023-09-09 02:59:36 +03:00
await pilot.click("CharInput", control=True)
assert snap_compare(PAINT, run_before=swap_selected_colors)
2023-09-09 06:38:37 +03:00
def test_paint_character_picker_dialog(snap_compare: SnapCompareType, each_theme: None):
async def open_character_picker(pilot: Pilot[None]):
# app.dark = True caused it to fail to open the dialog in the dark theme,
# due to `self.call_later(self.refresh_css)` in `watch_dark` in `App`
# (verified by replacing `app.dark = args.theme == "dark"` with `app.call_later(app.refresh_css)`)
# Adding a delay works around this.
await pilot.pause(1.0)
2023-09-09 02:59:36 +03:00
await pilot.click("CharInput")
await pilot.click("CharInput")
assert pilot.app.query_one("CharacterSelectorDialogWindow")
2023-09-09 02:59:36 +03:00
assert snap_compare(PAINT, run_before=open_character_picker, terminal_size=LARGER)
2023-09-09 06:38:37 +03:00
def test_paint_edit_colors_dialog(snap_compare: SnapCompareType, each_theme: None):
async def open_edit_colors(pilot: Pilot[None]):
await pilot.pause(1.0) # see comment in test_paint_character_picker_dialog
2023-09-09 02:59:36 +03:00
pilot.app.query("ColorsBox Button")[0].id = "a_color_button"
await pilot.click("#a_color_button")
await pilot.click("#a_color_button")
assert pilot.app.query_one("EditColorsDialogWindow")
2023-09-09 02:59:36 +03:00
assert snap_compare(PAINT, run_before=open_edit_colors, terminal_size=LARGEST)
2023-09-09 06:38:37 +03:00
def test_paint_expand_canvas_dialog(snap_compare: SnapCompareType, each_theme: None):
2023-09-09 04:40:29 +03:00
async def paste_large_content(pilot: Pilot[None]):
if TYPE_CHECKING:
# Will be a different class at runtime, per test, due to re-evaluating the module.
2023-09-09 06:47:09 +03:00
assert isinstance(pilot.app, PaintApp)
2023-09-09 04:40:29 +03:00
pilot.app.paste("a" * 1000)
assert snap_compare(PAINT, run_before=paste_large_content, terminal_size=LARGER)
2023-09-09 06:38:37 +03:00
def test_paint_error_dialog(snap_compare: SnapCompareType, each_theme: None):
async def show_error(pilot: Pilot[None]):
if TYPE_CHECKING:
# Will be a different class at runtime, per test, due to re-evaluating the module.
2023-09-09 06:47:09 +03:00
assert isinstance(pilot.app, PaintApp)
pilot.app.message_box("EMIT", "Error Message Itself Test", "ok", error=Exception("Error Message Itself Test"))
assert pilot.app.query_one("MessageBox")
await pilot.pause(1.0)
assert pilot.app.query_one("MessageBox .details_button")
await pilot.click("MessageBox .details_button")
await pilot.pause(0.5) # avoid pressed state
assert snap_compare(PAINT, run_before=show_error)
2023-09-09 06:38:37 +03:00
def test_paint_custom_zoom_dialog(snap_compare: SnapCompareType, each_theme: None):
2023-09-09 05:21:08 +03:00
async def show_custom_zoom(pilot: Pilot[None]):
if TYPE_CHECKING:
# Will be a different class at runtime, per test, due to re-evaluating the module.
2023-09-09 06:47:09 +03:00
assert isinstance(pilot.app, PaintApp)
2023-09-09 05:21:08 +03:00
pilot.app.action_custom_zoom()
assert snap_compare(PAINT, run_before=show_custom_zoom)
2023-09-09 06:38:37 +03:00
def test_paint_about_paint_dialog(snap_compare: SnapCompareType, each_theme: None):
2023-09-09 05:33:44 +03:00
async def show_about_paint(pilot: Pilot[None]):
if TYPE_CHECKING:
# Will be a different class at runtime, per test, due to re-evaluating the module.
2023-09-09 06:47:09 +03:00
assert isinstance(pilot.app, PaintApp)
2023-09-09 05:33:44 +03:00
pilot.app.action_about_paint()
assert snap_compare(PAINT, run_before=show_about_paint)
2023-09-13 04:22:22 +03:00
# TODO: test changing color of in-progress polygon when selecting a color from the palette
# TODO: test dragging to define polygon; in particular, dragging can define the first two points at once
2023-09-09 08:12:21 +03:00
def test_paint_polygon_tool(snap_compare: SnapCompareType):
async def draw_polygon(pilot: Pilot[None]):
2023-09-13 04:22:22 +03:00
# TODO: fix polygon closing prematurely, interpreting clicks as double clicks despite the distance,
# and then remove as many of these pause() calls as possible
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Polygon")
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(3, 2))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(19, 2))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(29, 7))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(11, 7))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(3, 2))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
# first shape (defined above) should be closed by returning to start point
await click_by_index(pilot, '#available_colors Button', 16) # red
await pilot.click('#canvas', offset=Offset(17, 10))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(30, 16))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(49, 16))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(35, 10))
# no pause — double click on purpose
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(35, 10))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
# second shape (defined above) should be closed by double clicking
await click_by_index(pilot, '#available_colors Button', 17) # yellow
await pilot.click('#canvas', offset=Offset(33, 2))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(58, 16))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(58, 2))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(44, 2))
await pilot.pause(0.3)
2023-09-12 04:51:27 +03:00
await pilot.click('#canvas', offset=Offset(52, 7))
# third shape (defined above) should be left open as a polyline
2023-09-09 08:12:21 +03:00
assert snap_compare(PAINT, run_before=draw_polygon, terminal_size=LARGER)
2023-09-11 20:24:44 +03:00
def test_text_tool_wrapping(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Text")
2023-09-12 05:36:31 +03:00
await drag(pilot, '#canvas', [Offset(5, 8), Offset(24, 16)])
2023-09-11 21:56:13 +03:00
for key in ('T', 'e', 'x', 't', 'space', 'T', 'o', 'o', 'l', 'space', 'T', 'e', 's', 't', 'space', 'left_parenthesis', 'T', 'T', 'T', 'right_parenthesis', 'n', 'e', 'w', 'space', 'l', 'i', 'n', 'e', 'space', 's', 't', 'a', 'r', 't', 's', 'space', 'h', 'e', 'r', 'e', 'a', 'n', 'd', 'space', 'h', 'e', 'r', 'e', 'space', 'a', 'u', 't', 'o', 'm', 'a', 't', 'i', 'c', 'a', 'l', 'hyphen', 'l', 'y'):
2023-09-11 20:24:44 +03:00
await pilot.press(key)
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
def test_text_tool_cursor_keys_and_color(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Text")
2023-09-12 05:36:31 +03:00
await drag(pilot, '#canvas', [Offset(8, 5), Offset(21, 10)])
for key in ('s', 'end', 'pagedown', '1', 'home', '2', 'pageup', '3', 'end', '4', 'pageup', 'home', 'right', 'right', 'c', 'r', 'e', 't', 'backspace', 'backspace', 'backspace', 'backspace', 'v', '3', 'n'):
await pilot.press(key)
2023-09-11 20:24:44 +03:00
await pilot.click('#canvas', offset=Offset(9, 10))
for key in ('e', 'r', 'o'):
await pilot.press(key)
2023-09-12 02:28:33 +03:00
await click_by_index(pilot, '#available_colors Button', 9)
await click_by_index(pilot, '#available_colors Button', 18, control=True)
2023-09-11 20:24:44 +03:00
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
2023-09-11 20:24:44 +03:00
2023-09-12 05:37:30 +03:00
def test_free_form_select(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Free-Form Select")
await drag(pilot, '#canvas', [Offset(8, 3), Offset(9, 4), Offset(10, 4), Offset(13, 5), Offset(17, 7), Offset(21, 8), Offset(26, 10), Offset(30, 12), Offset(35, 13), Offset(39, 14), Offset(40, 14), Offset(40, 15), Offset(39, 15), Offset(37, 15), Offset(33, 16), Offset(30, 16), Offset(25, 16), Offset(21, 16), Offset(16, 15), Offset(11, 14), Offset(4, 13), Offset(0, 13), Offset(2, 0), Offset(3, 0), Offset(4, 0), Offset(0, 9), Offset(7, 8), Offset(13, 7), Offset(20, 5), Offset(27, 4), Offset(28, 4), Offset(27, 4), Offset(27, 5), Offset(25, 6), Offset(20, 10), Offset(15, 13), Offset(12, 16), Offset(11, 17), Offset(10, 18), Offset(10, 17), Offset(10, 17)])
await pilot.press('ctrl+i')
await drag(pilot, '#canvas', [Offset(21, 14), Offset(21, 14), Offset(22, 14), Offset(23, 14), Offset(24, 14), Offset(25, 14), Offset(26, 14), Offset(27, 14), Offset(28, 14), Offset(29, 14), Offset(30, 14), Offset(31, 14), Offset(32, 14), Offset(33, 14), Offset(34, 14), Offset(34, 14)])
await drag(pilot, '#canvas', [Offset(9, 10), Offset(9, 10), Offset(8, 10), Offset(7, 10), Offset(6, 10), Offset(5, 11), Offset(4, 11), Offset(3, 11), Offset(4, 11), Offset(5, 11), Offset(6, 11), Offset(7, 11), Offset(8, 11), Offset(8, 12), Offset(9, 12), Offset(10, 12), Offset(11, 12), Offset(12, 12), Offset(13, 12), Offset(13, 11), Offset(13, 10), Offset(14, 10), Offset(14, 9), Offset(13, 9), Offset(12, 9), Offset(11, 9), Offset(11, 10), Offset(10, 10), Offset(10, 10)])
await pilot.press('ctrl+i')
await drag(pilot, '#canvas', [Offset(12, 6), Offset(12, 6), Offset(13, 6), Offset(14, 6), Offset(14, 5), Offset(15, 5), Offset(16, 5), Offset(16, 4), Offset(17, 4), Offset(18, 4), Offset(18, 3), Offset(19, 3), Offset(19, 2), Offset(20, 2), Offset(20, 1), Offset(19, 0), Offset(18, 0), Offset(17, 0), Offset(16, 0), Offset(15, 0), Offset(14, 0), Offset(13, 0), Offset(12, 0), Offset(12, 1), Offset(11, 1), Offset(10, 1), Offset(9, 2), Offset(8, 2), Offset(7, 3), Offset(6, 3), Offset(6, 3)])
await pilot.press('delete')
await drag(pilot, '#canvas', [Offset(47, 10), Offset(47, 10), Offset(46, 10), Offset(46, 11), Offset(45, 12), Offset(45, 13), Offset(45, 14), Offset(45, 15), Offset(45, 16), Offset(46, 17), Offset(47, 18), Offset(48, 18), Offset(49, 18), Offset(50, 18), Offset(50, 19), Offset(51, 19), Offset(52, 19), Offset(53, 19), Offset(54, 19), Offset(55, 19), Offset(56, 19), Offset(57, 18), Offset(58, 18), Offset(59, 17), Offset(60, 17), Offset(60, 16), Offset(61, 16), Offset(61, 15), Offset(61, 14), Offset(60, 14), Offset(60, 13), Offset(59, 12), Offset(58, 11), Offset(57, 11), Offset(57, 10), Offset(56, 10), Offset(55, 10), Offset(55, 10)])
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
def test_free_form_select_meld_negative_coords(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Fill With Color")
await click_by_index(pilot, '#available_colors Button', 17) # yellow
await pilot.click('#canvas', offset=Offset(19, 8))
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Free-Form Select")
await drag(pilot, '#editing_area', [Offset(19, 1), Offset(19, 1), Offset(18, 2), Offset(17, 2), Offset(15, 3), Offset(13, 4), Offset(6, 6), Offset(2, 8), Offset(0, 10), Offset(3, 2), Offset(2, 0), Offset(2, 1), Offset(2, 2), Offset(3, 2), Offset(5, 2), Offset(14, 14), Offset(1, 14), Offset(1, 13), Offset(4, 13), Offset(8, 12), Offset(12, 11), Offset(16, 11), Offset(20, 10), Offset(22, 10), Offset(23, 9), Offset(24, 9), Offset(25, 9), Offset(26, 9), Offset(26, 8), Offset(25, 8), Offset(23, 7), Offset(19, 6), Offset(15, 6), Offset(11, 5), Offset(6, 3), Offset(3, 2), Offset(2, 1), Offset(2, 0), Offset(3, 0), Offset(3, 0)])
await drag(pilot, '#canvas', [Offset(13, 8), Offset(13, 8), Offset(12, 8), Offset(12, 7), Offset(12, 6), Offset(11, 6), Offset(11, 5), Offset(10, 5), Offset(10, 4), Offset(9, 4), Offset(8, 3), Offset(8, 3)])
await pilot.press('ctrl+i')
await pilot.pause(0.5)
await pilot.click('#editing_area', offset=Offset(0, 20)) # deselect
await pilot.pause(0.5)
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
2023-09-12 05:37:30 +03:00
2023-09-12 05:42:10 +03:00
def test_select(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Select")
await drag(pilot, '#canvas', [Offset(5, 3), Offset(5, 3), Offset(6, 4), Offset(8, 5), Offset(9, 6), Offset(11, 7), Offset(13, 8), Offset(14, 10), Offset(15, 10), Offset(16, 11), Offset(17, 11), Offset(18, 12), Offset(19, 12), Offset(19, 13), Offset(20, 13), Offset(21, 13), Offset(21, 14), Offset(22, 14), Offset(22, 14)])
await pilot.press('ctrl+i')
await drag(pilot, '#canvas', [Offset(12, 8), Offset(13, 8), Offset(13, 9), Offset(14, 10), Offset(15, 12), Offset(16, 13), Offset(18, 14), Offset(19, 15), Offset(20, 15), Offset(20, 16), Offset(21, 16), Offset(22, 17), Offset(23, 17), Offset(24, 17), Offset(24, 16), Offset(24, 15), Offset(24, 14), Offset(25, 13), Offset(25, 13)])
await drag(pilot, '#canvas', [Offset(7, 13), Offset(7, 13), Offset(8, 13), Offset(9, 14), Offset(10, 15), Offset(11, 15), Offset(12, 16), Offset(13, 17), Offset(15, 17), Offset(16, 18), Offset(17, 19), Offset(18, 19), Offset(19, 19), Offset(19, 20), Offset(20, 20), Offset(21, 20), Offset(22, 21), Offset(23, 21), Offset(24, 21), Offset(24, 22), Offset(25, 22), Offset(25, 22)])
await pilot.press('ctrl+i')
await drag(pilot, '#canvas', [Offset(12, 3), Offset(12, 3), Offset(13, 4), Offset(14, 5), Offset(14, 6), Offset(15, 7), Offset(16, 7), Offset(17, 8), Offset(18, 9), Offset(19, 9), Offset(20, 10), Offset(21, 10), Offset(22, 10), Offset(23, 11), Offset(24, 12), Offset(25, 12), Offset(26, 12), Offset(26, 13), Offset(27, 13), Offset(28, 14), Offset(29, 14), Offset(30, 14), Offset(30, 15), Offset(31, 15), Offset(32, 15), Offset(32, 16), Offset(33, 16), Offset(34, 16), Offset(35, 16), Offset(36, 16), Offset(37, 16), Offset(38, 16), Offset(38, 15), Offset(39, 15), Offset(40, 15), Offset(41, 15), Offset(42, 16), Offset(41, 16), Offset(41, 16)])
await drag(pilot, '#canvas', [Offset(34, 13), Offset(34, 13), Offset(34, 12), Offset(35, 12), Offset(35, 11), Offset(35, 10), Offset(35, 9), Offset(36, 9), Offset(37, 9), Offset(38, 9), Offset(39, 9), Offset(40, 9), Offset(39, 9), Offset(38, 9), Offset(38, 9)],
control=True) # duplicate selection with Ctrl
await drag(pilot, '#canvas', [Offset(2, 6), Offset(2, 6), Offset(3, 6), Offset(4, 6), Offset(4, 7), Offset(5, 7), Offset(6, 7), Offset(7, 8), Offset(8, 8), Offset(8, 9), Offset(10, 9), Offset(11, 10), Offset(12, 11), Offset(13, 11), Offset(13, 12), Offset(15, 12), Offset(16, 13), Offset(17, 13), Offset(18, 14), Offset(19, 14), Offset(20, 15), Offset(21, 16), Offset(23, 16), Offset(24, 17), Offset(25, 17), Offset(25, 18), Offset(26, 18), Offset(27, 18), Offset(28, 18), Offset(29, 18), Offset(30, 18), Offset(31, 18), Offset(32, 18), Offset(33, 18), Offset(33, 18)])
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
2023-09-09 06:38:37 +03:00
def test_gallery_app(snap_compare: SnapCompareType):
2023-09-07 22:14:07 +03:00
assert snap_compare(GALLERY)