2023-09-09 06:38:37 +03:00
|
|
|
from pathlib import Path, PurePath
|
|
|
|
from typing import TYPE_CHECKING, Awaitable, Callable, Iterable, Protocol
|
2023-09-07 22:14:07 +03:00
|
|
|
|
|
|
|
import pytest
|
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-09 08:12:21 +03:00
|
|
|
from textual.widget import Widget
|
2023-09-08 22:03:50 +03:00
|
|
|
from textual.widgets import Input
|
2023-09-07 22:14:07 +03:00
|
|
|
|
2023-09-09 06:49:52 +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.
|
2023-09-09 03:20:07 +03:00
|
|
|
Input.cursor_blink = False # type: ignore
|
2023-09-09 05:12:29 +03:00
|
|
|
# paint.DOUBLE_CLICK_TIME = 20.0 # seconds; ridiculously high; probably ineffective since paint.py is re-evaluated for each test
|
Test light and dark theme variations with a pytest fixture
First I tried setting PYTEST_TEXTUAL_PAINT_ARGS as an environment variable, to be interpreted by args.py, but it turns out args.py is only executed once, not once per test. It's not using subprocesses, only importing and reimporting the app code, and instantiating new App instances, so parts of the code that are at the top level of modules is only evaluated once.
So I found a new strategy, of importing the `args` object in the test fixture and modifying it directly.
I also realized the --ascii-only option permanently modifies Textual's widgets and borders, and my own widgets, for the life of the process, so I'm holding off on that one. I should be able to make --ascii-only mode more dynamic, and could even target it as a runtime toggle, as a goal, since that's basically what I'll need to achieve to get it working for the tests, but thinking of it as a feature is more fun.
2023-09-08 01:40:16 +03:00
|
|
|
|
|
|
|
@pytest.fixture(params=[
|
|
|
|
{"theme": "light", "ascii_only": False},
|
|
|
|
{"theme": "dark", "ascii_only": False},
|
Merge snapshot results for ASCII-only and Unicode UI tests
I'm basically doing TDD to snapshot testing!
I'm creating tests that don't pass yet, setting up an expectation
that the app match the given screenshots, which is funny in a nice
"improper hierarchy" sort of way, but it's possible because I do
actually have the app rendering how I want, just only in isolation.
If I run the ascii_only tests by themselves, I can get good results
from them, but running them interwoven with default Unicode-using UI
tests doesn't work yet, since the ASCII-only mode permanently changes
how certain widgets render, for the life of the process, so that's
what I'm applying TDD to: making it toggleable at runtime.
I commented out the Unicode tests, and uncommented the ASCII-only tests,
renamed test_snapshots.ambr to test_snapshots_ascii.ambr,
reverted the changes to test_snapshots.ambr to get the Unicode version,
ran my new merge_ambr.py script to join the sets of snapshots,
then replaced test_snapshots.ambr with test_snapshots_merged.ambr
Finally, I uncommented both sets of tests, and I'm ready to do TDD!
2023-09-08 18:24:57 +03:00
|
|
|
{"theme": "light", "ascii_only": True},
|
|
|
|
{"theme": "dark", "ascii_only": True},
|
Test light and dark theme variations with a pytest fixture
First I tried setting PYTEST_TEXTUAL_PAINT_ARGS as an environment variable, to be interpreted by args.py, but it turns out args.py is only executed once, not once per test. It's not using subprocesses, only importing and reimporting the app code, and instantiating new App instances, so parts of the code that are at the top level of modules is only evaluated once.
So I found a new strategy, of importing the `args` object in the test fixture and modifying it directly.
I also realized the --ascii-only option permanently modifies Textual's widgets and borders, and my own widgets, for the life of the process, so I'm holding off on that one. I should be able to make --ascii-only mode more dynamic, and could even target it as a runtime toggle, as a goal, since that's basically what I'll need to achieve to get it working for the tests, but thinking of it as a feature is more fun.
2023-09-08 01:40:16 +03:00
|
|
|
], 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."""
|
Test light and dark theme variations with a pytest fixture
First I tried setting PYTEST_TEXTUAL_PAINT_ARGS as an environment variable, to be interpreted by args.py, but it turns out args.py is only executed once, not once per test. It's not using subprocesses, only importing and reimporting the app code, and instantiating new App instances, so parts of the code that are at the top level of modules is only evaluated once.
So I found a new strategy, of importing the `args` object in the test fixture and modifying it directly.
I also realized the --ascii-only option permanently modifies Textual's widgets and borders, and my own widgets, for the life of the process, so I'm holding off on that one. I should be able to make --ascii-only mode more dynamic, and could even target it as a runtime toggle, as a goal, since that's basically what I'll need to achieve to get it working for the tests, but thinking of it as a feature is more fun.
2023-09-08 01:40:16 +03:00
|
|
|
theme = request.param.get("theme")
|
|
|
|
ascii_only = request.param.get("ascii_only")
|
|
|
|
# os.environ["PYTEST_TEXTUAL_PAINT_ARGS"] = f"--theme {theme}" + (" --ascii-only" if ascii_only else "")
|
|
|
|
from textual_paint.args import args
|
|
|
|
args.theme = theme
|
|
|
|
args.ascii_only = ascii_only
|
|
|
|
yield
|
|
|
|
# del os.environ["PYTEST_TEXTUAL_PAINT_ARGS"]
|
|
|
|
args.theme = "light"
|
|
|
|
args.ascii_only = False
|
|
|
|
|
|
|
|
|
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):
|
2023-09-08 00:21:20 +03:00
|
|
|
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):
|
2023-09-08 00:21:20 +03:00
|
|
|
assert snap_compare(PAINT, press=["ctrl+e"])
|
|
|
|
|
2023-09-09 06:38:37 +03:00
|
|
|
def test_paint_open_dialog(snap_compare: SnapCompareType, each_theme: None):
|
2023-09-08 00:43:14 +03:00
|
|
|
assert snap_compare(PAINT, press=["ctrl+o"], terminal_size=LARGER)
|
2023-09-08 00:21:20 +03:00
|
|
|
|
2023-09-09 06:38:37 +03:00
|
|
|
def test_paint_save_dialog(snap_compare: SnapCompareType, each_theme: None):
|
2023-09-08 00:43:14 +03:00
|
|
|
assert snap_compare(PAINT, press=["ctrl+s"], terminal_size=LARGER)
|
2023-09-08 00:21:20 +03:00
|
|
|
|
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-08 00:21:20 +03:00
|
|
|
|
2023-09-09 06:38:37 +03:00
|
|
|
def test_paint_view_bitmap(snap_compare: SnapCompareType):
|
2023-09-08 00:21:20 +03:00
|
|
|
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):
|
2023-09-08 00:21:20 +03:00
|
|
|
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):
|
2023-09-09 04:09:08 +03:00
|
|
|
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")
|
2023-09-09 04:09:08 +03:00
|
|
|
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):
|
2023-09-09 04:09:08 +03:00
|
|
|
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")
|
2023-09-09 04:09:08 +03:00
|
|
|
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]):
|
2023-09-09 05:12:29 +03:00
|
|
|
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):
|
2023-09-09 05:12:29 +03:00
|
|
|
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)
|
2023-09-09 05:12:29 +03:00
|
|
|
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")
|
|
|
|
# pilot.app.query_one("MessageBox .details_button", Button).press()
|
|
|
|
await pilot.click("MessageBox .details_button")
|
2023-09-09 05:31:44 +03:00
|
|
|
await pilot.pause(0.5) # avoid pressed state
|
2023-09-09 05:12:29 +03:00
|
|
|
|
|
|
|
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-09 08:12:21 +03:00
|
|
|
def test_paint_polygon_tool(snap_compare: SnapCompareType):
|
|
|
|
async def draw_polygon(pilot: Pilot[None]):
|
|
|
|
tool_buttons = pilot.app.query("ToolsBox Button")
|
|
|
|
color_buttons = pilot.app.query("ColorsBox Button")
|
|
|
|
for button in tool_buttons:
|
|
|
|
if button.tooltip == "Polygon":
|
|
|
|
polygon_tool_button = button
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise Exception("Couldn't find Polygon tool button")
|
|
|
|
|
|
|
|
async def clickity(button: Widget) -> None:
|
|
|
|
button.add_class("to_click")
|
|
|
|
await pilot.click(".to_click")
|
|
|
|
button.remove_class("to_click")
|
|
|
|
await pilot.pause(1.0) # for good luck
|
|
|
|
|
|
|
|
await clickity(polygon_tool_button)
|
|
|
|
await pilot.click("Canvas", offset=Offset(2, 2))
|
2023-09-09 08:31:00 +03:00
|
|
|
await pilot.click("Canvas", offset=Offset(2, 20))
|
|
|
|
await pilot.click("Canvas", offset=Offset(30, 20))
|
|
|
|
await pilot.click("Canvas", offset=Offset(30, 2))
|
2023-09-09 08:12:21 +03:00
|
|
|
await pilot.click("Canvas", offset=Offset(2, 2)) # end by clicking on the start point
|
2023-09-09 08:31:00 +03:00
|
|
|
# await clickity(color_buttons[16]) # red
|
|
|
|
# await pilot.click("Canvas", offset=Offset(10, 5))
|
|
|
|
# await pilot.click("Canvas", offset=Offset(10, 9))
|
|
|
|
# await pilot.click("Canvas", offset=Offset(10, 9))
|
|
|
|
# await pilot.click("Canvas", offset=Offset(1, 5))
|
|
|
|
# await pilot.click("Canvas", offset=Offset(1, 5)) # end by double clicking
|
|
|
|
# await clickity(color_buttons[17]) # yellow
|
|
|
|
# await pilot.click("Canvas", offset=Offset(10, 13))
|
|
|
|
# await pilot.click("Canvas", offset=Offset(15, 13))
|
|
|
|
# await pilot.click("Canvas", offset=Offset(12, 16)) # don't end, leave as polyline
|
2023-09-09 08:12:21 +03:00
|
|
|
|
|
|
|
assert snap_compare(PAINT, run_before=draw_polygon, 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)
|
|
|
|
|