Commit Graph

131 Commits

Author SHA1 Message Date
Isaiah Odhner
d8206659c6 Make fill tool compare colors numerically (with a threshold) 2023-09-22 13:17:39 -04:00
Isaiah Odhner
f85e161e9e Test fill tool on a spiral 2023-09-22 13:16:16 -04:00
Isaiah Odhner
fae2c216f0 Optimize Color and Style construction in Canvas
This does not change anything visually, but the snapshots are changed
because the IDs use a hash which includes color names, and the color
names changed from rgb() style to hex.
2023-09-21 20:36:09 -04:00
Isaiah Odhner
cd3137a737 Use new Collapsible widget for error details 2023-09-18 23:43:59 -04:00
Isaiah Odhner
268380a0c6 Update textual to 0.36.0
All tests pass... but pyright shows the signature for app.exit changed.
2023-09-18 22:37:01 -04:00
Isaiah Odhner
40220e22d2 Fix flaky CharacterSelectorDialogWindow test (hopefully)
This should fix this failure:

    FAILED tests/test_snapshots.py::test_paint_character_picker_dialog[dark_unicode] - textual.css.query.NoMatches: No nodes match <DOMQuery query='CharacterSelectorDialogWindow'>

I noticed this first in a Windows VM, and am now seeing it in Ubuntu,
so it might have to do with the test running slowly.
This was back on textual 0.28.0 by the way; it doesn't have to do with
the recent updates (as far as I know; at least, not entirely.)

I've never had it reproduce when running in isolation with
    pytest tests/test_snapshots.py::test_paint_character_picker_dialog

I tried adding a delay right before the query, and that DIDN'T work,
I got the failure at least once with that in place, so I think it was
failing to detect a double click, rather than querying while the window
was in the process of opening, and so I decided to try increasing the
double click threshold. The click() method of pilot has a cumulative
artificial delay of 0.3s, so two clicks is at least 0.6s and it's not
hard to imagine the event processing pushing that over 0.8s.
I actually created the `DOUBLE_CLICK_TIME` to allow overriding it in
tests, and I'm not sure if this actually works to override it.
2023-09-18 02:00:58 -04:00
Isaiah Odhner
35a6845ab5 Update textual to 0.33.0
I had to fix the layout of a few dialogs where elements decided they
wanted to start expanding a lot more than before.
I'm guessing this has to do with the changelog entry:
    "Fixed relative units not always expanding auto containers"
    https://github.com/Textualize/textual/pull/3059

The snapshot changes are basically bogus. The before and after are
visually identical, with the difference view showing all black.

Since there were a lot of switches to toggle and I had to wait for the
snapshot tests to run (slow!), I wrote a little automation to toggle
"Show difference" for all the results:

    document.querySelectorAll("#flexSwitchCheckDefault").forEach((element)=> element.click())

It would be good to have this ability in the snapshot report UI itself,
maybe even replacing the individual toggles, although I'm not sure about
that, especially since it might be laggy toggling the blend modes with
a lot of test results. (I suppose if that was really an issue, it could
toggle all the visible test results and then toggle others as they come
into view, though that's a bit more complex.)

As for understanding the structural changes to the snapshots, I tried
making a visualization using hue, coloring according to the position
of a rect within the list of rects:

    const richTerminals = document.querySelectorAll(".rich-terminal");
    
    richTerminals.forEach(function(terminal) {
        const rectElements = terminal.querySelectorAll("rect");
        
        rectElements.forEach(function(rect, index) {
            const fraction = index / (rectElements.length - 1);
            const cycles = 40;
            const hue = fraction * cycles * 360;
            rect.style.fill = `hsl(${hue}, 100%, 50%)`;
        });
    });

This shows some difference, but it isn't very elucidating, since the
structural changes only show as gradual shifts in the hue, and affect
other rects even if said rects are identical, so it's subtle and messy.

Coloring based on a hash proves to actually highlight differences:

    const richTerminals = document.querySelectorAll(".rich-terminal");
    
    richTerminals.forEach(function(terminal) {
        const rectElements = terminal.querySelectorAll("rect");
        
        rectElements.forEach(function(rect, index) {
            const hash = hash(rect.outerHTML);
            const hue = (hash % 360 + 360) % 360;
            rect.style.fill = `hsl(${hue}, 100%, 50%)`;
        });
    });
    
    function hash(s) {
        let hash = 0;
        for (let i = 0; i < s.length; i++) {
            const char = s.charCodeAt(i);
            hash = (hash << 5) - hash + char;
        }
        return hash;
    }

As for analyzing the differences now visible, eh, "maybe later."
2023-09-18 01:08:22 -04:00
Isaiah Odhner
28d9a2ff04 Update textual to 0.29.0
This only slightly affects the exact lightness of the grayed out radio button labels, at least as far as the tests cover.
2023-09-17 00:56:21 -04:00
Isaiah Odhner
06344fb8de Silence type checker warnings (reportOptionalMemberAccess) 2023-09-16 23:31:08 -04:00
Isaiah Odhner
0791b1c080 Satisfy the type checker
`PYRIGHT_PYTHON_FORCE_VERSION=1.1.327 pyright` now gives 0 errors

(before this commit it was 16 errors)
2023-09-16 23:31:08 -04:00
Isaiah Odhner
a6b5cb31be Clean up mocked method FigletFont.preloadFont 2023-09-16 23:31:08 -04:00
Isaiah Odhner
0f617dd8c4 Dynamically theme message box icons 2023-09-15 22:03:34 -04:00
Isaiah Odhner
7a13659d48 Fix "Show Details" not changing to "Hide Details" when expanding error 2023-09-15 20:44:40 -04:00
Isaiah Odhner
fc1baa9815 Move imports so that Organize Imports doesn't break it
Before, if I ran Organize Imports in VS Code, it reordered things such that `sys.path.insert` was after the import that needed it.
Now it's stable.
2023-09-15 19:56:27 -04:00
Isaiah Odhner
857e459b2d Get tests running, if not passing, on Windows 2023-09-15 17:08:13 -04:00
Isaiah Odhner
40291f29fd Add fake folders to mask OS differences in snapshot tests
Now the tests pass on macOS, not just Ubuntu. Not sure about Windows.
2023-09-15 17:08:13 -04:00
Isaiah Odhner
7fa80b853c Fix flaky test due to pressed button style 2023-09-14 20:22:48 -04:00
Isaiah Odhner
91b2c285fe Re-color in-progress polygon/curve immediately 2023-09-14 20:04:31 -04:00
Isaiah Odhner
ad39f34084 Add Polygon test with dragging 2023-09-14 16:58:47 -04:00
Isaiah Odhner
3f1c6c964a Fix Polygon tool detecting double clicks despite distance 2023-09-14 16:32:40 -04:00
Isaiah Odhner
b6d0d2cc7b Delete debug scratchpad for polygon test 2023-09-14 15:09:22 -04:00
Isaiah Odhner
bf8e445f69 Make palette state local to the app instance 2023-09-14 02:31:49 -04:00
Isaiah Odhner
faa9c76ba8 Test file drag-and-drop handling 2023-09-14 02:03:52 -04:00
Isaiah Odhner
070e253394 Move fixtures to conftest.py 2023-09-14 01:17:01 -04:00
Isaiah Odhner
5a7ba995c0 Accept super trivial change to About dialog snapshots
The scrollbar handle is slightly taller now because I removed the `--recode-samples` option.
2023-09-13 02:37:37 -04:00
Isaiah Odhner
c538787ff2 Recurse into subfolders for re-encoding round trip test
There are many more failures now, which I should probably take a look at
at some point, but I don't know how to best inspect the changes yet.
I'll mark these as xfail (expected to fail) for now.
2023-09-13 02:24:25 -04:00
Isaiah Odhner
bb047c04e7 Move --recode-samples to an automated test 2023-09-12 21:54:19 -04:00
Isaiah Odhner
6f13a5a302 Clean up 2023-09-12 21:23:36 -04:00
Isaiah Odhner
1f1bf577f2 Disable tool icon swaps when running in pytest 2023-09-12 21:08:24 -04:00
Isaiah Odhner
08847ca91b Fix Free-Form Select behavior when melding with negative coordinates 2023-09-12 20:16:55 -04:00
Isaiah Odhner
96fe28269d Accept changes to file dialog snapshots
...but NOT the Free-Form Select test! I'm noticing that when marked
as skipped, running with --snapshot-update deletes the snapshot.
So neither xfail or skip (or xfail with run=False) is a good solution.
It's almost as if people don't normally do TDD with snapshots.

Anyways, HOPEFULLY I can move on now, to actually fixing things,
like the Polygon tool, and new bugs I've found while adding tests.
2023-09-12 19:45:41 -04:00
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
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