Commit Graph

1450 Commits

Author SHA1 Message Date
Isaiah Odhner
d6705912b3 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-12 19:45:41 -04:00
Isaiah Odhner
dce55cd833 Fix further errors with pyfakefs 2023-09-12 19:45:41 -04:00
Isaiah Odhner
7f91138a63 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-12 19:45:41 -04:00
Isaiah Odhner
370b497cc6 VS Code: Add launch configuration for debugging tests
I just want to disable the external code skipping feature ("justMyCode")
but I seem to have to abandon the pytest integration to do so?
2023-09-12 19:45:41 -04:00
Isaiah Odhner
bb8e968f6c Test Free-Form Select off-screen melding bug 2023-09-12 19:45:41 -04:00
Isaiah Odhner
48b15319da Test Select tool 2023-09-11 22:42:10 -04:00
Isaiah Odhner
2206df20bd Test Free-Form Select tool 2023-09-11 22:37:30 -04:00
Isaiah Odhner
ad86491ada Shorten Offset arguments 2023-09-11 22:36:31 -04:00
Isaiah Odhner
42e3104c7e Tighten Polygon test timing
and find the tool button by tooltip text
2023-09-11 22:25:38 -04:00
Isaiah Odhner
ec774c264a Test Polygon tool! 2023-09-11 22:11:02 -04:00
Isaiah Odhner
e849af1dcd Add to todos 2023-09-11 21:42:09 -04:00
Isaiah Odhner
304f18c3f4 Import helpers instead of including them in generated code 2023-09-11 21:42:09 -04:00
Isaiah Odhner
85d601b262 Refactor tests that click a tool button 2023-09-11 21:16:13 -04:00
Isaiah Odhner
6e7a3445e0 DRY widget clicking helpers (use one in the other) 2023-09-11 20:23:15 -04:00
Isaiah Odhner
95d89a6e7f Move Pilot test helpers to a file 2023-09-11 19:40:20 -04:00
Isaiah Odhner
7f379103a6 Accept new Text tool test snapshots 2023-09-11 19:35:04 -04:00
Isaiah Odhner
0d9f62a4c3 Skip polygon test for now 2023-09-11 19:24:41 -04:00
Isaiah Odhner
6c108294b8 Add modifier options to click_by_index (not recorded yet) 2023-09-11 19:12:26 -04:00
Isaiah Odhner
d6399022ee Fix/cleanup test_text_tool_cursor_keys_and_color
- Fix `APP_PATH` reference error
- Hold Ctrl to set the foreground color (modifiers not recorded yet)
- DRY `pilot.press`
- Use same terminal size as many other tests
- Seven isn't spelt with a 2, but zero is! ;)
2023-09-11 19:10:18 -04:00
Isaiah Odhner
3b76ba0997 Accept mysterious ID changes to file dialog snapshots
WITHOUT accepting new WIP Polygon and Text tool tests
2023-09-11 18:45:21 -04:00
Isaiah Odhner
5426ed14a3 Use semantic key synonym 2023-09-11 18:02:20 -04:00
Isaiah Odhner
155b0e4c99 Wait only at start of playback, not before each drag/click in test 2023-09-11 18:02:20 -04:00
Isaiah Odhner
2b447a06d6 WIP: Polygon and Text tool tests 2023-09-11 18:02:20 -04:00
Isaiah Odhner
58ddc7e856 Add a word in docstring
I would've named the class `TestRecorder` if the file name `test_recorder.py` wouldn't be picked up by pytest as a test file...
2023-09-11 18:02:20 -04:00
Isaiah Odhner
2d03709565 Rename placeholders to sound less hand-wavy 2023-09-11 18:02:20 -04:00
Isaiah Odhner
a844cd7967 Make output_file a parameter; add __init__ docstring 2023-09-11 18:02:20 -04:00
Isaiah Odhner
405cc1bf4e Make test recorder generic to apps
The only mention of Paint is at construction time now.
2023-09-11 18:02:20 -04:00
Isaiah Odhner
c5e96ef580 Add todos to module-level docstring 2023-09-11 18:02:20 -04:00
Isaiah Odhner
021bc8a93c Simplify escaping of triple quotes 2023-09-11 18:02:20 -04:00
Isaiah Odhner
8614787e3d Add newline to separate related code 2023-09-11 18:02:20 -04:00
Isaiah Odhner
340c7ce74e Add Ctrl+R to restart and replay the recording 2023-09-11 18:02:20 -04:00
Isaiah Odhner
84ff381f04 This might handle multi-screen apps (untested) 2023-09-11 18:02:20 -04:00
Isaiah Odhner
f62c8ff6c9 Highlight active step during playback
I love when GitHub Copilot suggests code using an API (or language feature / idiom etc.) that I didn't even know about.
More often it'll just make something up, but `highlight_lines` actually exists!

Anyways, I'm doing a hacky thing to get it to highlight lines of code during execution,
prepending code (invisibly) to each line, except lines that would cause a syntax error.

Also, the steps view isn't updating reliably (even when adding steps).
2023-09-11 18:02:20 -04:00
Isaiah Odhner
5e55846fb2 Add steps view
This isn't very useful yet without highlighting of the current step in playback.
2023-09-11 18:02:20 -04:00
Isaiah Odhner
b590d807a7 Add todo/thoughts about drag vs click ambiguity 2023-09-11 18:02:20 -04:00
Isaiah Odhner
5b0d8c703a Refactor: use set instead of dict for helper codes
Don't need arbitrary IDs for them.
2023-09-11 18:02:20 -04:00
Isaiah Odhner
87c9c192e0 Disable event comments in generated code 2023-09-11 18:02:20 -04:00
Isaiah Odhner
196ae0e810 Speed up drags during playback 2023-09-11 18:02:20 -04:00
Isaiah Odhner
1a53a18912 Fix IndexError when undoing all the way 2023-09-11 18:02:20 -04:00
Isaiah Odhner
402b46c7df Make failing to create a selector non-fatal 2023-09-11 18:02:20 -04:00
Isaiah Odhner
d6d9ddba67 Add a todo 2023-09-11 18:02:20 -04:00
Isaiah Odhner
9739422936 Show every event with comments in generated code 2023-09-11 18:02:20 -04:00
Isaiah Odhner
baa2b8cbc6 Fix batch undoing for drags 2023-09-11 18:02:20 -04:00
Isaiah Odhner
0e9f6d28a7 Implement recording/replaying drags 2023-09-11 18:02:20 -04:00
Isaiah Odhner
52f319da24 Refactor: split method
(This is probably pointless.)
2023-09-11 17:59:56 -04:00
Isaiah Odhner
82a5b201d4 WIP: make test recorder more generic to apps 2023-09-11 17:59:56 -04:00
Isaiah Odhner
2fb9e64c4f DRY generated code with a helper function 2023-09-11 17:59:56 -04:00
Isaiah Odhner
b9503e44f4 This is somewhat more reliable (maybe just due to the delay....) 2023-09-11 17:59:56 -04:00
Isaiah Odhner
2a7b4412e1 Add docstrings, rename method
The new method name reflects the fact that it handles some input as commands, not just recording the events.
2023-09-11 17:59:56 -04:00
Isaiah Odhner
96ef2dfdff Whitespace: organize imports 2023-09-11 17:59:56 -04:00