Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_quartodoc-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ quartodoc:
contents:
- playwright.controller.InputActionButton
- playwright.controller.InputActionLink
- playwright.controller.InputBookmarkButton
- playwright.controller.InputCheckbox
- playwright.controller.InputCheckboxGroup
- playwright.controller.InputDarkMode
Expand Down Expand Up @@ -59,6 +60,7 @@ quartodoc:
- playwright.controller.NavsetTab
- playwright.controller.NavsetUnderline
- playwright.controller.NavPanel
- playwright.controller.PageNavbar
- title: Upload and download
desc: Methods for interacting with Shiny app uploading and downloading controller.
contents:
Expand Down
19 changes: 15 additions & 4 deletions shiny/playwright/controller/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,13 +1058,21 @@ def expect_class_state(
"cell-edit-editing", timeout=timeout
)
elif value == "editing":
self.expect_cell_class("cell-edit-editing", row=row, col=col)
self.expect_cell_class(
"cell-edit-editing", row=row, col=col, timeout=timeout
)
elif value == "saving":
self.expect_cell_class("cell-edit-saving", row=row, col=col)
self.expect_cell_class(
"cell-edit-saving", row=row, col=col, timeout=timeout
)
elif value == "failure":
self.expect_cell_class("cell-edit-failure", row=row, col=col)
self.expect_cell_class(
"cell-edit-failure", row=row, col=col, timeout=timeout
)
elif value == "success":
self.expect_cell_class("cell-edit-success", row=row, col=col)
self.expect_cell_class(
"cell-edit-success", row=row, col=col, timeout=timeout
)
else:
raise ValueError(
"Invalid state. Select one of 'success', 'failure', 'saving', 'editing', 'ready'"
Expand Down Expand Up @@ -1096,6 +1104,9 @@ def _edit_cell_no_save(

self._cell_scroll_if_needed(row=row, col=col, timeout=timeout)
cell.dblclick(timeout=timeout)

# Wait for the cell to enter editing mode before filling the textarea
expect_to_have_class(cell, "cell-edit-editing", timeout=timeout)
cell.locator("> textarea").fill(text)

def set_sort(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,24 @@ def test_validate_row_selection_in_edit_mode(
data_frame._edit_cell_no_save("Temp value", row=1, col=16)
page.keyboard.press("Escape")
page.keyboard.press("Enter")
data_frame.expect_class_state(
"editing",
row=1,
col=0,
) # Stage column begins to be edited.

# This interaction (Enter key to start editing) can be flaky, so we retry if needed
max_retries = 3
for attempt in range(max_retries):
try:
page.wait_for_timeout(100)
data_frame.expect_class_state(
"editing",
row=1,
col=0,
timeout=1000,
)
break
except AssertionError:
if attempt < max_retries - 1:
page.keyboard.press("Enter")
else:
raise

# Click outside the table/Press Escape to exit row focus.
# Tab to the column name, hit enter. Verify the table becomes sorted.
Expand Down
98 changes: 98 additions & 0 deletions tests/pytest/test_controller_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ast
from pathlib import Path
from typing import Set

import pytest
import yaml

CONTROLLER_DIR = Path("shiny/playwright/controller")
DOCS_CONFIG = Path("docs/_quartodoc-testing.yml")
SKIP_PATTERNS = {"Base", "Container", "Label", "StyleM"}
CONTROLLER_BASE_PATTERNS = {
"Base",
"Container",
"Label",
"StyleM",
"WidthLocM",
"InputActionButton",
"UiBase",
"UiWithLabel",
"UiWithContainer",
}


def _is_valid_controller_class(node: ast.ClassDef) -> bool:
class_name = node.name
base_names = {ast.unparse(base) for base in node.bases}

return (
not class_name.startswith("_")
and not any(pattern in class_name for pattern in SKIP_PATTERNS)
and not any(base.endswith("P") for base in base_names if isinstance(base, str))
and any(
base.startswith("_") or any(p in base for p in CONTROLLER_BASE_PATTERNS)
for base in base_names
)
)


def get_controller_classes() -> Set[str]:
classes: Set[str] = set()
for py_file in CONTROLLER_DIR.glob("*.py"):
if py_file.name == "__init__.py":
continue
try:
tree = ast.parse(py_file.read_text(encoding="utf-8"))
classes.update(
node.name
for node in ast.walk(tree)
if isinstance(node, ast.ClassDef) and _is_valid_controller_class(node)
)
except Exception as e:
pytest.fail(f"Failed to parse {py_file}: {e}")
return classes


def get_documented_controllers() -> Set[str]:
try:
config = yaml.safe_load(DOCS_CONFIG.read_text(encoding="utf-8"))
except Exception as e:
pytest.fail(f"Failed to load or parse {DOCS_CONFIG}: {e}")

return {
content.split(".")[-1]
for section in config.get("quartodoc", {}).get("sections", [])
for content in section.get("contents", [])
if isinstance(content, str) and content.startswith("playwright.controller.")
}


def test_all_controllers_are_documented():
controller_classes = get_controller_classes()
documented_controllers = get_documented_controllers()

missing_from_docs = controller_classes - documented_controllers
extra_in_docs = documented_controllers - controller_classes

from typing import List

error_messages: List[str] = []
if missing_from_docs:
missing_list = "\n".join(
sorted(f" - playwright.controller.{c}" for c in missing_from_docs)
)
error_messages.append(
f"Controllers missing from {DOCS_CONFIG}:\n{missing_list}"
)

if extra_in_docs:
extra_list = "\n".join(
sorted(f" - playwright.controller.{c}" for c in extra_in_docs)
)
error_messages.append(f"Extraneous classes in {DOCS_CONFIG}:\n{extra_list}")

if error_messages:
pytest.fail("\n\n".join(error_messages), pytrace=False)

assert controller_classes, "No controller classes were found."
assert documented_controllers, "No documented controllers were found."
Loading