textual-paint/tests/test_snapshots.py
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

267 lines
18 KiB
Python

"""Visual regression tests, using pytest-textual-snapshot.
Run with `pytest tests/test_snapshots.py`, or `pytest` to run all tests.
"""
from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Awaitable, Callable, Iterable, Protocol
from pyfakefs.fake_filesystem import FakeFilesystem
from textual.geometry import Offset
from textual.pilot import Pilot
from textual.widgets import Input
from tests.pilot_helpers import click_by_attr, click_by_index, drag
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
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:
...
# 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"
LARGER = (81, 38)
"""Large enough to show the Textual Paint app's main UI and most dialogs comfortably."""
LARGEST = (107, 42)
"""Large enough to show the Edit Colors dialog, which is a bit oversized."""
# Prevent flaky tests due to timing issues.
Input.cursor_blink = False # type: ignore
def test_paint_app(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, terminal_size=LARGER)
def test_paint_stretch_skew_dialog(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+w"])
def test_paint_flip_rotate_dialog(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+r"])
def test_paint_image_attributes_dialog(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+e"])
def test_paint_open_dialog(snap_compare: SnapCompareType, each_theme: None, my_fs: FakeFilesystem):
assert snap_compare(PAINT, press=["ctrl+o"], terminal_size=LARGER)
def test_paint_save_dialog(snap_compare: SnapCompareType, each_theme: None, my_fs: FakeFilesystem):
assert snap_compare(PAINT, press=["ctrl+s"], terminal_size=LARGER)
def test_paint_help_dialog(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["f1"], terminal_size=LARGER)
def test_paint_view_bitmap(snap_compare: SnapCompareType):
assert snap_compare(PAINT, press=["ctrl+f"])
def test_paint_invert_and_exit(snap_compare: SnapCompareType, each_theme: None):
assert snap_compare(PAINT, press=["ctrl+i", "ctrl+q"])
def test_swap_selected_colors(snap_compare: SnapCompareType):
async def swap_selected_colors(pilot: Pilot[None]):
await pilot.click("CharInput", control=True)
assert snap_compare(PAINT, run_before=swap_selected_colors)
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)
await pilot.click("CharInput")
await pilot.click("CharInput")
assert pilot.app.query_one("CharacterSelectorDialogWindow")
assert snap_compare(PAINT, run_before=open_character_picker, terminal_size=LARGER)
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
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")
assert snap_compare(PAINT, run_before=open_edit_colors, terminal_size=LARGEST)
def test_paint_expand_canvas_dialog(snap_compare: SnapCompareType, each_theme: None):
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.
assert isinstance(pilot.app, PaintApp)
pilot.app.paste("a" * 1000)
assert snap_compare(PAINT, run_before=paste_large_content, terminal_size=LARGER)
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.
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)
def test_paint_custom_zoom_dialog(snap_compare: SnapCompareType, each_theme: None):
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.
assert isinstance(pilot.app, PaintApp)
pilot.app.action_custom_zoom()
assert snap_compare(PAINT, run_before=show_custom_zoom)
def test_paint_about_paint_dialog(snap_compare: SnapCompareType, each_theme: None):
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.
assert isinstance(pilot.app, PaintApp)
pilot.app.action_about_paint()
assert snap_compare(PAINT, run_before=show_about_paint)
def test_paint_polygon_tool(snap_compare: SnapCompareType):
async def draw_polygon(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Polygon")
await pilot.click('#canvas', offset=Offset(3, 2))
await pilot.click('#canvas', offset=Offset(19, 2))
await pilot.click('#canvas', offset=Offset(29, 7))
await pilot.click('#canvas', offset=Offset(11, 7))
await pilot.click('#canvas', offset=Offset(3, 2))
# 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.click('#canvas', offset=Offset(30, 16))
await pilot.click('#canvas', offset=Offset(49, 16))
# double click:
await pilot.click('#canvas', offset=Offset(35, 10))
await pilot.click('#canvas', offset=Offset(35, 10))
# second shape (defined above) should be closed by double clicking
await pilot.click('#canvas', offset=Offset(33, 2))
await pilot.click('#canvas', offset=Offset(58, 16))
await pilot.click('#canvas', offset=Offset(58, 2))
await pilot.click('#canvas', offset=Offset(44, 2))
await pilot.click('#canvas', offset=Offset(52, 7))
# third shape (defined above) should be left open as a polyline
# Selecting a color should immediately update the in-progress shape:
await click_by_index(pilot, '#available_colors Button', 17) # yellow
await pilot.pause(0.5) # avoid pressed state
assert snap_compare(PAINT, run_before=draw_polygon, terminal_size=LARGER)
def test_paint_polygon_tool_dragging(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Polygon")
# dragging can define the first two points at once
await drag(pilot, '#canvas', [Offset(11, 4), Offset(38, 6)])
await pilot.click('#canvas', offset=Offset(23, 16))
await drag(pilot, '#canvas', [Offset(38, 16), Offset(65, 14)])
# FIXME: click at previous location should not be considered a double click
# due to the distance of the drag
await pilot.pause(1.0) # REMOVE ME WHEN FIXED
await pilot.click('#canvas', offset=Offset(65, 14))
await drag(pilot, '#canvas', [Offset(52, 19), Offset(8, 19)])
await pilot.click('#canvas', offset=Offset(21, 11))
await pilot.pause(1.0) # prevent double click
await pilot.click('#canvas', offset=Offset(21, 11))
# double click, with slightly different locations (within threshold):
await pilot.click('#canvas', offset=Offset(2, 11))
await pilot.click('#canvas', offset=Offset(3, 11))
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
def test_text_tool_wrapping(snap_compare: SnapCompareType):
async def automate_app(pilot: Pilot[None]):
await click_by_attr(pilot, "ToolsBox Button", "tooltip", "Text")
await drag(pilot, '#canvas', [Offset(5, 8), Offset(24, 16)])
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'):
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")
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)
await pilot.click('#canvas', offset=Offset(9, 10))
for key in ('e', 'r', 'o'):
await pilot.press(key)
await click_by_index(pilot, '#available_colors Button', 9)
await click_by_index(pilot, '#available_colors Button', 18, control=True)
assert snap_compare(PAINT, run_before=automate_app, terminal_size=LARGER)
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)
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)
def test_gallery_app(snap_compare: SnapCompareType):
assert snap_compare(GALLERY)