mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-14 10:18:31 -06:00
more accessibility improvements and frontend tests
This commit is contained in:
@@ -11,6 +11,7 @@ type AbstractButtonState = ComponentState & {
|
||||
color: ColorSet;
|
||||
content: ComponentId;
|
||||
is_sensitive: boolean;
|
||||
accessibility_label: string | null;
|
||||
};
|
||||
|
||||
abstract class AbstractButtonComponent extends ComponentBase<AbstractButtonState> {
|
||||
@@ -30,6 +31,7 @@ abstract class AbstractButtonComponent extends ComponentBase<AbstractButtonState
|
||||
// Create the element
|
||||
let element = document.createElement("div");
|
||||
element.classList.add("rio-button");
|
||||
element.role = "button";
|
||||
|
||||
this.childContainer = document.createElement("div");
|
||||
element.appendChild(this.childContainer);
|
||||
@@ -122,6 +124,18 @@ abstract class AbstractButtonComponent extends ComponentBase<AbstractButtonState
|
||||
"rio-insensitive",
|
||||
!deltaState.is_sensitive
|
||||
);
|
||||
this.buttonElement.ariaDisabled = deltaState.is_sensitive
|
||||
? "false"
|
||||
: "true";
|
||||
}
|
||||
|
||||
// Accessibility label
|
||||
if (deltaState.accessibility_label !== undefined) {
|
||||
if (deltaState.accessibility_label === null) {
|
||||
this.buttonElement.removeAttribute("aria-label");
|
||||
} else {
|
||||
this.buttonElement.ariaLabel = deltaState.accessibility_label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class CalendarComponent extends ComponentBase<CalendarState> {
|
||||
// These store the displayed year and month. This is in contrast to the
|
||||
// *selected* year and month, which are stored in the state.
|
||||
private displayedYear: number;
|
||||
private displayedMonth: number; // [1, 12]
|
||||
private displayedMonth: number; // 1 to 12
|
||||
|
||||
createElement(): HTMLElement {
|
||||
// Create the HTML structure
|
||||
|
||||
@@ -8,8 +8,8 @@ type MouseButton = "left" | "middle" | "right";
|
||||
export type PointerEventListenerState = ComponentState & {
|
||||
_type_: "PointerEventListener-builtin";
|
||||
content: ComponentId;
|
||||
reportPress: MouseButton[];
|
||||
reportDoublePress: MouseButton[];
|
||||
reportPress: boolean;
|
||||
reportDoublePress: boolean;
|
||||
reportPointerDown: MouseButton[];
|
||||
reportPointerUp: MouseButton[];
|
||||
reportPointerMove: boolean;
|
||||
@@ -50,7 +50,7 @@ export class PointerEventListenerComponent extends ComponentBase<PointerEventLis
|
||||
let reportDoublePress =
|
||||
deltaState.reportDoublePress ?? this.state.reportDoublePress;
|
||||
|
||||
if (reportPress.length > 0 || reportDoublePress.length > 0) {
|
||||
if (reportPress || reportDoublePress) {
|
||||
this.element.onclick = this._onClick.bind(this);
|
||||
} else {
|
||||
this.element.onclick = null;
|
||||
@@ -129,8 +129,13 @@ export class PointerEventListenerComponent extends ComponentBase<PointerEventLis
|
||||
private _onClick(event: MouseEvent): void {
|
||||
// This handler is responsible for both single clicks and double clicks
|
||||
|
||||
// If it's not a left-click, ignore it
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Double click
|
||||
if (eventMatchesButton(event, this.state.reportDoublePress)) {
|
||||
if (this.state.reportDoublePress) {
|
||||
let timeout = this._doubleClickTimeoutByButton[event.button];
|
||||
|
||||
if (timeout === undefined) {
|
||||
@@ -140,7 +145,7 @@ export class PointerEventListenerComponent extends ComponentBase<PointerEventLis
|
||||
window.setTimeout(() => {
|
||||
// Send a "press" event and clear the timeout so that
|
||||
// the next press starts a new timeout
|
||||
if (eventMatchesButton(event, this.state.reportPress)) {
|
||||
if (this.state.reportPress) {
|
||||
this._sendEventToBackend("press", event, false);
|
||||
}
|
||||
|
||||
@@ -165,7 +170,7 @@ export class PointerEventListenerComponent extends ComponentBase<PointerEventLis
|
||||
|
||||
// We know that there's no double click handler for this button, so we
|
||||
// can just send a "press" event without worrying about anything else
|
||||
if (eventMatchesButton(event, this.state.reportPress)) {
|
||||
if (this.state.reportPress) {
|
||||
this._sendEventToBackend("press", event, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class SliderComponent extends ComponentBase<SliderState> {
|
||||
// Create the HTML structure
|
||||
let element = document.createElement("div");
|
||||
element.classList.add("rio-slider");
|
||||
element.role = "slider";
|
||||
element.innerHTML = `
|
||||
<div class="rio-slider-column">
|
||||
<div class="rio-slider-inner">
|
||||
@@ -171,12 +172,19 @@ export class SliderComponent extends ComponentBase<SliderState> {
|
||||
|
||||
this.minValueElement.textContent = minimum.toFixed(2);
|
||||
this.maxValueElement.textContent = maximum.toFixed(2);
|
||||
|
||||
// Update accessibility properties
|
||||
this.element.ariaValueMin = minimum.toString();
|
||||
this.element.ariaValueMax = maximum.toString();
|
||||
this.element.ariaValueNow = value.toString();
|
||||
}
|
||||
|
||||
if (deltaState.is_sensitive === true) {
|
||||
this.element.classList.remove("rio-disabled-input");
|
||||
this.element.ariaDisabled = "false";
|
||||
} else if (deltaState.is_sensitive === false) {
|
||||
this.element.classList.add("rio-disabled-input");
|
||||
this.element.ariaDisabled = "true";
|
||||
}
|
||||
|
||||
if (deltaState.show_values === true) {
|
||||
|
||||
@@ -58,10 +58,12 @@ export class SwitchComponent extends ComponentBase<SwitchState> {
|
||||
|
||||
if (deltaState.is_sensitive === true) {
|
||||
this.element.classList.remove("rio-switcheroo-disabled");
|
||||
|
||||
let checkbox = this.element.querySelector("input");
|
||||
checkbox!.disabled = false;
|
||||
} else if (deltaState.is_sensitive === false) {
|
||||
this.element.classList.add("rio-switcheroo-disabled");
|
||||
|
||||
let checkbox = this.element.querySelector("input");
|
||||
checkbox!.disabled = true;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ dependencies = [
|
||||
"crawlerdetect>=0.1.7,<0.4",
|
||||
"fastapi>=0.110,<0.116",
|
||||
"gitignore-parser>=0.1.11,<0.2",
|
||||
"imy[docstrings,deprecations]>=0.7post0,<0.8",
|
||||
"introspection>=1.9.9,<2.0",
|
||||
"imy[docstrings,deprecations]>=0.7.1,<0.8",
|
||||
"introspection>=1.9.11,<2.0",
|
||||
"isort>=5.13,<7.0",
|
||||
"langcodes>=3.4,<4.0",
|
||||
"multipart>=1.2,<2.0",
|
||||
|
||||
@@ -64,6 +64,10 @@ class Button(Component):
|
||||
|
||||
`on_press`: Triggered when the user clicks on the button.
|
||||
|
||||
`accessibility_label`: A short text description of the button for screen
|
||||
readers. If omitted and the `content` is a string, the `content` is
|
||||
used as the label.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -123,6 +127,7 @@ class Button(Component):
|
||||
is_sensitive: bool = True
|
||||
is_loading: bool = False
|
||||
on_press: rio.EventHandler[[]] = None
|
||||
accessibility_label: str | None = None
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
# Prepare the child
|
||||
@@ -195,6 +200,7 @@ class Button(Component):
|
||||
is_loading=self.is_loading,
|
||||
min_width=8 if isinstance(self.content, str) else 0,
|
||||
min_height=2.2,
|
||||
accessibility_label=self.accessibility_label,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -217,6 +223,7 @@ class _ButtonInternal(FundamentalComponent):
|
||||
color: rio.ColorSet
|
||||
is_sensitive: bool
|
||||
is_loading: bool
|
||||
accessibility_label: str | None
|
||||
|
||||
def _custom_serialize_(self) -> JsonDoc:
|
||||
if self.style == "plain":
|
||||
@@ -231,7 +238,13 @@ class _ButtonInternal(FundamentalComponent):
|
||||
"style": "plain-text",
|
||||
}
|
||||
|
||||
return {}
|
||||
accessibility_label = self.accessibility_label
|
||||
if accessibility_label is None:
|
||||
content = self.content
|
||||
if isinstance(content, str):
|
||||
accessibility_label = content
|
||||
|
||||
return {"accessibility_label": accessibility_label}
|
||||
|
||||
async def _on_message_(self, msg: t.Any) -> None:
|
||||
# Parse the message
|
||||
|
||||
@@ -286,9 +286,7 @@ class Component(abc.ABC, metaclass=ComponentMeta):
|
||||
|
||||
_: dataclasses.KW_ONLY
|
||||
|
||||
# Unfortunately we have to inline the `Key` type here because dataclasses
|
||||
# will create constructor signatures where `Key` can't be resolved.
|
||||
key: str | int | None = internal_field(default=None, init=True)
|
||||
key: Key | None = internal_field(default=None, init=True)
|
||||
|
||||
min_width: float = 0
|
||||
min_height: float = 0
|
||||
|
||||
@@ -205,6 +205,7 @@ class DateInput(Component):
|
||||
on_confirm=self._on_confirm,
|
||||
style=self.style,
|
||||
min_width=11,
|
||||
accessibility_label=self.accessibility_label,
|
||||
),
|
||||
rio.Icon(
|
||||
"material/calendar_today:fill",
|
||||
|
||||
@@ -57,6 +57,9 @@ class IconButton(Component):
|
||||
|
||||
`on_press`: Triggered when the user clicks on the button.
|
||||
|
||||
`accessibility_label`: A short text describing the purpose of the button for
|
||||
screen readers. If omitted, the icon name is used.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -114,6 +117,7 @@ class IconButton(Component):
|
||||
is_sensitive: bool
|
||||
min_size: float
|
||||
on_press: rio.EventHandler[[]]
|
||||
accessibility_label: str | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -139,7 +143,9 @@ class IconButton(Component):
|
||||
align_x: float | None = None,
|
||||
align_y: float | None = None,
|
||||
# SCROLLING-REWORK scroll_x: t.Literal["never", "auto", "always"] = "never",
|
||||
# SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] = "never",
|
||||
# SCROLLING-REWORK scroll_y: t.Literal["never", "auto", "always"] =
|
||||
# "never",
|
||||
accessibility_label: str | None = None,
|
||||
accessibility_role: AccessibilityRole | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
@@ -166,8 +172,15 @@ class IconButton(Component):
|
||||
self.color = color
|
||||
self.is_sensitive = is_sensitive
|
||||
self.on_press = on_press
|
||||
self.accessibility_label = accessibility_label
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
accessibility_label = self.accessibility_label
|
||||
if accessibility_label is None:
|
||||
accessibility_label = self.icon.partition("/")[-1]
|
||||
accessibility_label = accessibility_label.partition(":")[-1]
|
||||
accessibility_label = accessibility_label.replace("_", " ")
|
||||
|
||||
return _IconButtonInternal(
|
||||
on_press=self.on_press,
|
||||
content=rio.Icon(self.icon, min_width=0, min_height=0),
|
||||
@@ -176,6 +189,7 @@ class IconButton(Component):
|
||||
is_sensitive=self.is_sensitive,
|
||||
min_width=self.min_size,
|
||||
min_height=self.min_size,
|
||||
accessibility_label=accessibility_label,
|
||||
)
|
||||
|
||||
def _get_debug_details_(self) -> dict[str, t.Any]:
|
||||
@@ -194,6 +208,7 @@ class _IconButtonInternal(FundamentalComponent):
|
||||
color: rio.ColorSet
|
||||
is_sensitive: bool
|
||||
on_press: rio.EventHandler[[]]
|
||||
accessibility_label: str | None
|
||||
shape: t.Literal["circle"] = "circle"
|
||||
|
||||
def _custom_serialize_(self) -> JsonDoc:
|
||||
|
||||
@@ -81,10 +81,10 @@ class PointerEvent:
|
||||
|
||||
assert result.pointer_type in ("mouse", "touch")
|
||||
assert result.button is None or result.button in MOUSE_BUTTONS
|
||||
assert isinstance(result.window_x, float)
|
||||
assert isinstance(result.window_y, float)
|
||||
assert isinstance(result.component_x, float)
|
||||
assert isinstance(result.component_y, float)
|
||||
assert isinstance(result.window_x, (int, float))
|
||||
assert isinstance(result.window_y, (int, float))
|
||||
assert isinstance(result.component_x, (int, float))
|
||||
assert isinstance(result.component_y, (int, float))
|
||||
|
||||
return result
|
||||
|
||||
@@ -215,15 +215,15 @@ class PointerEventListener(FundamentalComponent):
|
||||
|
||||
# Dispatch the correct event
|
||||
if msg_type == "press":
|
||||
await self._call_appropriate_event_handler(
|
||||
self.on_press,
|
||||
PointerEvent._from_message(msg),
|
||||
)
|
||||
event = PointerEvent._from_message(msg)
|
||||
assert event.button == "left"
|
||||
await self.call_event_handler(self.on_press, event)
|
||||
|
||||
elif msg_type == "doublePress":
|
||||
event = PointerEvent._from_message(msg)
|
||||
assert event.button == "left"
|
||||
await self._call_appropriate_event_handler(
|
||||
self.on_double_press,
|
||||
PointerEvent._from_message(msg),
|
||||
self.on_double_press, event
|
||||
)
|
||||
|
||||
elif msg_type == "pointerDown":
|
||||
|
||||
@@ -15,6 +15,7 @@ from .fundamental_component import FundamentalComponent
|
||||
__all__ = [
|
||||
"SwitcherBarChangeEvent",
|
||||
"SwitcherBar",
|
||||
"SwitcherBarItem",
|
||||
]
|
||||
|
||||
T = t.TypeVar("T")
|
||||
@@ -39,6 +40,13 @@ class SwitcherBarChangeEvent(t.Generic[T]):
|
||||
value: T | None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SwitcherBarItem(t.Generic[T]):
|
||||
value: T
|
||||
name: str
|
||||
icon: str | None = None
|
||||
|
||||
|
||||
@t.final
|
||||
class SwitcherBar(FundamentalComponent, t.Generic[T]):
|
||||
"""
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import dataclasses
|
||||
import typing as t
|
||||
|
||||
import typing_extensions as te
|
||||
import rio
|
||||
|
||||
from .component import AccessibilityRole, Component, Key
|
||||
|
||||
__all__ = ["Tab", "Tabs"]
|
||||
__all__ = ["TabItem", "Tabs"]
|
||||
|
||||
|
||||
class Tab(t.TypedDict):
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class TabItem:
|
||||
name: str
|
||||
icon: te.NotRequired[str]
|
||||
content: Component
|
||||
icon: str | None = None
|
||||
|
||||
|
||||
class Tabs(Component):
|
||||
tabs: tuple[Tab, ...]
|
||||
tabs: t.Sequence[TabItem]
|
||||
active_tab_index: int = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*tabs: Tab,
|
||||
*tabs: TabItem,
|
||||
key: Key | None = None,
|
||||
margin: float | None = None,
|
||||
margin_x: float | None = None,
|
||||
@@ -62,3 +65,20 @@ class Tabs(Component):
|
||||
)
|
||||
|
||||
self.tabs = tabs
|
||||
|
||||
def build(self) -> Component:
|
||||
try:
|
||||
content = self.tabs[self.active_tab_index].content
|
||||
except IndexError:
|
||||
content = rio.Spacer()
|
||||
|
||||
return rio.Column(
|
||||
rio.SwitcherBar(
|
||||
*[
|
||||
rio.SwitcherBarItem(tab.name, icon=tab.icon, value=index)
|
||||
for index, tab in enumerate(self.tabs)
|
||||
],
|
||||
selected_value=self.active_tab_index,
|
||||
),
|
||||
rio.Container(content, grow_y=True),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from uniserde import JsonDoc
|
||||
import rio
|
||||
import rio.app_server
|
||||
|
||||
from ..components.component import Key
|
||||
from ..transports import MessageRecorderTransport
|
||||
|
||||
__all__ = ["BaseClient"]
|
||||
@@ -191,22 +192,32 @@ class BaseClient(abc.ABC):
|
||||
def root_component(self) -> rio.Component:
|
||||
return self.session._get_user_root_component()
|
||||
|
||||
def get_components(self, component_type: type[C]) -> t.Iterator[C]:
|
||||
def get_components(
|
||||
self,
|
||||
component_type: type[C] = rio.Component,
|
||||
key: Key | None = None,
|
||||
) -> t.Iterator[C]:
|
||||
roots = [self.root_component]
|
||||
|
||||
for root_component in roots:
|
||||
for component in root_component._iter_component_tree_():
|
||||
if type(component) is component_type:
|
||||
yield component # type: ignore
|
||||
if isinstance(component, component_type) and (
|
||||
key is None or key == component.key
|
||||
):
|
||||
yield component
|
||||
|
||||
roots.extend(
|
||||
dialog._root_component
|
||||
for dialog in component._owned_dialogs_.values()
|
||||
)
|
||||
|
||||
def get_component(self, component_type: type[C]) -> C:
|
||||
def get_component(
|
||||
self,
|
||||
component_type: type[C] = rio.Component,
|
||||
key: Key | None = None,
|
||||
) -> C:
|
||||
try:
|
||||
return next(self.get_components(component_type))
|
||||
return next(self.get_components(component_type, key=key))
|
||||
except StopIteration:
|
||||
raise ValueError(f"No component of type {component_type} found")
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
import asyncio_atexit
|
||||
@@ -11,6 +12,7 @@ import uvicorn
|
||||
import rio.app_server
|
||||
import rio.data_models
|
||||
from rio.app_server import FastapiServer
|
||||
from rio.components.component import Key
|
||||
from rio.components.text import Text
|
||||
from rio.transports import FastapiWebsocketTransport, MultiTransport
|
||||
from rio.utils import choose_free_port
|
||||
@@ -20,11 +22,12 @@ from .base_client import BaseClient
|
||||
__all__ = ["BrowserClient", "prepare_browser_client"]
|
||||
|
||||
|
||||
# For debugging. Set this to a number > 0 if you want to look at the browser.
|
||||
# When the debugger is active, we'll enable debugging features like making the
|
||||
# browser visible, visualizing clicks, etc.
|
||||
#
|
||||
# Note: Chrome's console doesn't show `console.debug` messages per default. To
|
||||
# see them, click on "All levels" and check "Verbose".
|
||||
DEBUG_SHOW_BROWSER_DURATION = 0
|
||||
DEBUGGER_ACTIVE = sys.gettrace() is not None
|
||||
|
||||
|
||||
server_manager: ServerManager | None = None
|
||||
@@ -73,6 +76,7 @@ async def prepare_browser_client():
|
||||
class BrowserClient(BaseClient):
|
||||
def __post_init__(self) -> None:
|
||||
self._page: playwright.async_api.Page | None = None
|
||||
self._page_closed_event = asyncio.Event()
|
||||
|
||||
@property
|
||||
def playwright_page(self) -> playwright.async_api.Page:
|
||||
@@ -93,7 +97,24 @@ class BrowserClient(BaseClient):
|
||||
|
||||
return self._page.viewport_size["height"]
|
||||
|
||||
async def click(self, x: float, y: float, *, sleep: float = 0.1) -> None:
|
||||
async def wait_for_component_to_exist(
|
||||
self,
|
||||
component_type: type[rio.Component] = rio.Component,
|
||||
key: Key | None = None,
|
||||
) -> None:
|
||||
assert self._page is not None
|
||||
|
||||
component = self.get_component(key=key, component_type=component_type)
|
||||
await self._page.wait_for_selector(f'[dbg-id="{component._id_}"]')
|
||||
|
||||
async def click(
|
||||
self,
|
||||
x: float,
|
||||
y: float,
|
||||
*,
|
||||
button: t.Literal["left", "middle", "right"] = "left",
|
||||
sleep: float = 0.1,
|
||||
) -> None:
|
||||
assert self._page is not None
|
||||
|
||||
if isinstance(x, float) and x <= 1:
|
||||
@@ -102,9 +123,10 @@ class BrowserClient(BaseClient):
|
||||
if isinstance(y, float) and y <= 1:
|
||||
y = round(y * self._window_height_in_pixels)
|
||||
|
||||
if DEBUG_SHOW_BROWSER_DURATION > 0:
|
||||
if DEBUGGER_ACTIVE:
|
||||
await self.execute_js(f"""
|
||||
const marker = document.createElement('div');
|
||||
marker.style.pointerEvents = 'none';
|
||||
marker.style.background = 'red';
|
||||
marker.style.borderRadius = '50%';
|
||||
marker.style.position = 'absolute';
|
||||
@@ -121,9 +143,20 @@ class BrowserClient(BaseClient):
|
||||
}}, {sleep} * 1000);
|
||||
""")
|
||||
|
||||
await self._page.mouse.click(x, y)
|
||||
await self._page.mouse.click(x, y, button=button)
|
||||
await asyncio.sleep(sleep)
|
||||
|
||||
async def double_click(
|
||||
self,
|
||||
x: float,
|
||||
y: float,
|
||||
*,
|
||||
button: t.Literal["left", "middle", "right"] = "left",
|
||||
sleep: float = 0.1,
|
||||
) -> None:
|
||||
await self.click(x, y, button=button, sleep=0.1)
|
||||
await self.click(x, y, button=button, sleep=sleep)
|
||||
|
||||
async def execute_js(self, js: str) -> t.Any:
|
||||
assert self._page is not None
|
||||
return await self._page.evaluate(js)
|
||||
@@ -148,6 +181,8 @@ class BrowserClient(BaseClient):
|
||||
)
|
||||
|
||||
self._page = await manager.new_page()
|
||||
self._page.on("close", lambda _: self._page_closed_event.set())
|
||||
|
||||
await self._page.goto(f"http://localhost:{manager.port}")
|
||||
|
||||
while True:
|
||||
@@ -157,8 +192,8 @@ class BrowserClient(BaseClient):
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def __aexit__(self, *args: t.Any) -> None:
|
||||
# Sleep to keep the browser open for debugging
|
||||
await asyncio.sleep(DEBUG_SHOW_BROWSER_DURATION)
|
||||
if DEBUGGER_ACTIVE:
|
||||
await self._page_closed_event.wait()
|
||||
|
||||
if self._page is not None:
|
||||
await self._page.close()
|
||||
@@ -273,7 +308,7 @@ class ServerManager:
|
||||
|
||||
try:
|
||||
self._browser = await self._playwright.chromium.launch(
|
||||
headless=DEBUG_SHOW_BROWSER_DURATION == 0
|
||||
headless=not DEBUGGER_ACTIVE
|
||||
)
|
||||
except Exception:
|
||||
raise Exception(
|
||||
|
||||
@@ -4,7 +4,7 @@ import typing as t
|
||||
import pytest
|
||||
|
||||
import rio
|
||||
from tests.utils.layouting import BrowserClient
|
||||
from rio.testing import BrowserClient
|
||||
|
||||
|
||||
class DialogOpener(rio.Component):
|
||||
|
||||
58
tests/test_frontend/test_pointer_event_listener.py
Normal file
58
tests/test_frontend/test_pointer_event_listener.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import typing as t
|
||||
|
||||
import pytest
|
||||
|
||||
import rio
|
||||
from rio.testing import BrowserClient
|
||||
|
||||
|
||||
@pytest.mark.parametrize("button", ["left", "middle", "right"])
|
||||
async def test_on_press_event(
|
||||
button: t.Literal["left", "middle", "right"],
|
||||
) -> None:
|
||||
event: rio.PointerEvent | None = None
|
||||
|
||||
def on_press(e: rio.PointerEvent):
|
||||
nonlocal event
|
||||
event = e
|
||||
|
||||
def build():
|
||||
return rio.PointerEventListener(rio.Spacer(), on_press=on_press)
|
||||
|
||||
async with BrowserClient(build) as client:
|
||||
await client.click(0.5, 0.5, button=button, sleep=0.5)
|
||||
|
||||
if button != "left":
|
||||
assert event is None
|
||||
return
|
||||
|
||||
assert event is not None
|
||||
assert event.button == "left"
|
||||
assert event.pointer_type == "mouse"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("button", ["left", "middle", "right"])
|
||||
async def test_on_double_press_event(
|
||||
button: t.Literal["left", "middle", "right"],
|
||||
) -> None:
|
||||
event: rio.PointerEvent | None = None
|
||||
|
||||
def on_double_press(e: rio.PointerEvent):
|
||||
nonlocal event
|
||||
event = e
|
||||
|
||||
def build():
|
||||
return rio.PointerEventListener(
|
||||
rio.Spacer(), on_double_press=on_double_press
|
||||
)
|
||||
|
||||
async with BrowserClient(build) as client:
|
||||
await client.double_click(0.5, 0.5, button=button, sleep=0.5)
|
||||
|
||||
if button != "left":
|
||||
assert event is None
|
||||
return
|
||||
|
||||
assert event is not None
|
||||
assert event.button == "left"
|
||||
assert event.pointer_type == "mouse"
|
||||
Reference in New Issue
Block a user