Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e77de89
Add page_navset and Navset_hidden tests and controllers
karangattu Sep 5, 2024
7314f12
Add tests for checking gap params
karangattu Sep 5, 2024
b6f3acb
address failing tests
karangattu Sep 6, 2024
30b846a
Merge branch 'main' into add-page_nav_bar-kitchensink
karangattu Sep 6, 2024
21f43f5
use expect_to_have_class
karangattu Sep 6, 2024
3fcc0e5
Add tests to check for page window title
karangattu Sep 11, 2024
714f7e1
address linting failures
karangattu Sep 11, 2024
b9d3baa
Add additional test to check for window title
karangattu Sep 11, 2024
80dcfe7
fix card tests
karangattu Sep 11, 2024
23a5938
Merge branch 'main' into add-page_nav_bar-kitchensink
schloerke Sep 11, 2024
8f56222
Change param name to `expect_inverse(value=)`; Add missing docs
schloerke Sep 11, 2024
35d8357
Add TODO
schloerke Sep 11, 2024
fcb433d
Merge branch 'add-page_nav_bar-kitchensink' of https://github.com/pos…
schloerke Sep 11, 2024
6efde5d
Remove default value of `expect_inverse(value=)`. This means it now r…
schloerke Sep 11, 2024
464f0c2
Added changelog
karangattu Sep 12, 2024
fe6d3a2
move change log around
schloerke Sep 12, 2024
dd9e1d5
Added NavsetPillList `.expect_widths()` method
karangattu Sep 12, 2024
109f9f1
Update test_page_navbar_fillable_app.py
karangattu Sep 12, 2024
5629ee9
Update _navs.py
karangattu Sep 12, 2024
69135d8
Update _navs.py
karangattu Sep 12, 2024
1514f86
Merge branch 'main' into add-page_nav_bar-kitchensink
karangattu Sep 12, 2024
1ec046f
Remove unused imports
karangattu Sep 12, 2024
f8dbcd4
linting issues
karangattu Sep 12, 2024
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 shiny/playwright/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
NavsetPillList,
NavsetTab,
NavsetUnderline,
PageNavbar,
)
from ._output import (
OutputCode,
Expand Down Expand Up @@ -121,4 +122,5 @@
"NavsetUnderline",
"DownloadButton",
"DownloadLink",
"PageNavbar",
]
163 changes: 136 additions & 27 deletions shiny/playwright/controller/_navs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
from typing_extensions import Literal

from .._types import PatternOrStr, Timeout
from ..expect import (
expect_not_to_have_class,
expect_to_have_class,
expect_to_have_style,
)
from ..expect._internal import expect_attribute_to_have_value
from ..expect._internal import expect_class_to_have_value as _expect_class_to_have_value
from ..expect._internal import expect_style_to_have_value as _expect_style_to_have_value
from ._base import (
InitLocator,
UiWithContainer,
Expand Down Expand Up @@ -424,18 +429,20 @@ def __init__(self, page: Page, id: str) -> None:
loc="> li.nav-item",
)

def expect_well(self, has_well: bool, *, timeout: Timeout = None) -> None:
# TODO-karan test this method
def expect_well(self, value: bool, *, timeout: Timeout = None) -> None:
"""
Expects the navset pill list to have a well.

Parameters
----------
has_well
`True` if the navset pill list is expected to have a well, `False` otherwise.
value
`True` if the navset pill list is expected to be constructed with a well,
`False` otherwise.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
if has_well:
if value:
playwright_expect(self.loc_container.locator("..")).to_have_class("well")
else:
playwright_expect(self.loc_container.locator("..")).not_to_have_class(
Expand Down Expand Up @@ -563,12 +570,12 @@ def __init__(self, page: Page, id: str) -> None:
)


class NavsetBar(
class _NavsetBarBase(
_ExpectNavsetSidebarM,
_ExpectNavsetTitleM,
_NavsetBase,
):
"""Controller for :func:`shiny.ui.navset_bar`."""
"""Mixin class for common expectations of nav bars."""

def __init__(self, page: Page, id: str) -> None:
"""
Expand Down Expand Up @@ -631,20 +638,30 @@ def expect_position(
re.compile(rf"{position}"), timeout=timeout
)

def expect_inverse(self, *, timeout: Timeout = None) -> None:
def expect_inverse(
self,
value: bool,
*,
timeout: Timeout = None,
) -> None:
"""
Expects the navset bar to be light text color if inverse is True

Parameters
----------
value
`True` if the navset bar is expected to have inverse text color, `False` otherwise.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
playwright_expect(self._loc_navbar).to_have_class(
re.compile("navbar-inverse"), timeout=timeout
_expect_class_to_have_value(
self._loc_navbar,
"navbar-inverse",
has_class=value,
timeout=timeout,
)

def expect_bg(self, bg: str, *, timeout: Timeout = None) -> None:
def expect_bg(self, bg: PatternOrStr, *, timeout: Timeout = None) -> None:
"""
Expects the navset bar to have the specified background color.

Expand All @@ -655,11 +672,14 @@ def expect_bg(self, bg: str, *, timeout: Timeout = None) -> None:
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
_expect_style_to_have_value(
self._loc_navbar, "background-color", f"{bg} !important", timeout=timeout
expect_to_have_style(
self._loc_navbar,
"background-color",
f"{bg} !important",
timeout=timeout,
)

def expect_gap(self, gap: str, *, timeout: Timeout = None) -> None:
def expect_gap(self, gap: PatternOrStr, *, timeout: Timeout = None) -> None:
"""
Expects the navset bar to have the specified gap.

Expand All @@ -670,28 +690,117 @@ def expect_gap(self, gap: str, *, timeout: Timeout = None) -> None:
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
_expect_style_to_have_value(
self.get_loc_active_content(), "gap", gap, timeout=timeout
expect_to_have_style(
self.get_loc_active_content(),
"gap",
gap,
timeout=timeout,
)

def expect_layout(
self, layout: Literal["fluid", "fixed"] = "fluid", *, timeout: Timeout = None
def expect_fluid(
self,
value: bool,
*,
timeout: Timeout = None,
) -> None:
"""
Expects the navset bar to have the specified layout.
Expects the navset bar to have a fluid or fixed layout.

Parameters
----------
layout
The expected layout.
value
`True` if the layout is `fluid` or `False` if it is `fixed`.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
if layout == "fluid":
playwright_expect(
self.loc_container.locator("..").locator("..")
).to_have_class(re.compile("container-fluid"), timeout=timeout)
if value:
expect_to_have_class(
self._loc_navbar.locator("> div"),
"container-fluid",
timeout=timeout,
)
else:
playwright_expect(self.loc_container.locator("..")).to_have_class(
re.compile("container"), timeout=timeout
expect_to_have_class(
self._loc_navbar.locator("> div"),
"container",
timeout=timeout,
)


class NavsetBar(_NavsetBarBase):
"""Controller for :func:`shiny.ui.navset_bar`."""


class PageNavbar(_NavsetBarBase):
"""Controller for :func:`shiny.ui.page_navbar`."""

def expect_fillable(self, *, timeout: Timeout = None) -> None:
"""
Expects the main content area to be considered a fillable (i.e., flexbox) container

Parameters
----------
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
expect_to_have_class(
self.get_loc_active_content(), "html-fill-container", timeout=timeout
)

def expect_fillable_mobile(self, value: bool, *, timeout: Timeout = None) -> None:
"""
Expects the main content area to be considered a fillable (i.e., flexbox) container on mobile

Parameters
----------
value
`True` if the main content area is expected to be fillable on mobile, `False` otherwise.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
# This is important since fillable_mobile needs fillable property to be True
expect_to_have_class(
self.page.locator("body"), "bslib-page-fill", timeout=timeout
)

if value:
expect_not_to_have_class(
self.page.locator("body"), "bslib-flow-mobile", timeout=timeout
)
else:
expect_to_have_class(
self.page.locator("body"), "bslib-flow-mobile", timeout=timeout
)

def expect_window_title(
self, title: PatternOrStr, *, timeout: Timeout = None
) -> None:
"""
Expects the window title to have the specified text.

Parameters
----------
title
The expected window title.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
playwright_expect(self.page).to_have_title(title, timeout=timeout)

def expect_lang(self, lang: PatternOrStr, *, timeout: Timeout = None) -> None:
"""
Expects the HTML tag to have the specified language.

Parameters
----------
lang
The expected language.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
expect_attribute_to_have_value(
self.page.locator("html"),
"lang",
lang,
timeout=timeout,
)
38 changes: 38 additions & 0 deletions shiny/playwright/expect/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,41 @@ def expect_class_to_have_value(
expect_to_have_class(loc, class_, timeout=timeout)
else:
expect_not_to_have_class(loc, class_, timeout=timeout)


def _expect_nav_to_have_header_footer(
parent_loc: Locator,
header_id: str,
footer_id: str,
*,
timeout: Timeout = None,
) -> None:
"""
Expect the DOM structure for a header and footer to be preserved.

Parameters
----------
parent_loc
The parent locator to check.
header_id
The ID of the header element.
footer_id
The ID of the footer element.
timeout
The maximum time to wait for the header and footer to appear.
"""
# assert the DOM structure for page_navbar with header and footer is preserved
class_attr = parent_loc.get_attribute("class")
if class_attr and "card" in class_attr:
complicated_parent_loc = parent_loc.locator(
"xpath=.",
has=parent_loc.locator("..").locator(
f".card-body:has(#{header_id}) + .card-body:has(.tab-content) + .card-body #{footer_id}"
),
)
else:
complicated_parent_loc = parent_loc.locator(
"xpath=.",
has=parent_loc.locator(f"#{header_id} + .tab-content + #{footer_id}"),
).locator("..")
playwright_expect(complicated_parent_loc).to_have_count(1, timeout=timeout)
5 changes: 0 additions & 5 deletions tests/playwright/shiny/components/nav/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
from shiny.types import NavSetArg
from shiny.ui import Sidebar

# TODO-karan; Make test that uses sidebar / no sidebar (where possible)
# TODO-karan; Make test that has/does not have a header & footer (where possible)
# TODO-karan; Test for title value (where possible)
# TODO-karan; Make test that has placement = "above" / "below" (where possible); Test in combination of with/without sidebar


def nav_controls(prefix: str) -> List[NavSetArg]:
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ def test_navset_bar_kitchensink(page: Page, local_app: ShinyAppProc) -> None:
"Panel A content"
)
navset_bar_with_sidebar_collapsible_bg_inverse.expect_position("sticky-top")
navset_bar_with_sidebar_collapsible_bg_inverse.expect_inverse()
navset_bar_with_sidebar_collapsible_bg_inverse.expect_inverse(value=True)
navset_bar_with_sidebar_collapsible_bg_inverse.expect_bg("DodgerBlue")
navset_bar_with_sidebar_collapsible_bg_inverse.expect_sidebar(True)
navset_bar_with_sidebar_collapsible_bg_inverse.expect_layout("fluid")
navset_bar_with_sidebar_collapsible_bg_inverse.expect_fluid(True)

navset_tab.set("fixed")
navset_bar_collapsible_underline_fixed_gap._expect_content_text("Panel A content")
navset_bar_collapsible_underline_fixed_gap.expect_value("A")
navset_bar_collapsible_underline_fixed_gap.expect_gap("50px")
navset_bar_collapsible_underline_fixed_gap.expect_layout("fixed")
navset_bar_collapsible_underline_fixed_gap.expect_fluid(False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from shiny import reactive
from shiny.express import input, ui

ui.input_radio_buttons("controller1", "Controller1", ["1", "2", "3"], selected="2")

with ui.navset_hidden(
id="hidden_tabs1",
header=ui.tags.div("Navset_hidden_header", id="navset_hidden_header1"),
footer=ui.tags.div("Navset_hidden_footer", id="navset_hidden_footer1"),
selected="panel2",
):
with ui.nav_panel(None, value="panel1"):
"Panel 1 content"
with ui.nav_panel(None, value="panel2"):
"Panel 2 content"
with ui.nav_panel(None, value="panel3"):
"Panel 3 content"

ui.markdown("-----")
ui.input_radio_buttons("controller2", "Controller2", ["4", "5", "6"])

with ui.navset_hidden(id="hidden_tabs2"):
with ui.nav_panel(None, value="panel4"):
"Panel 4 content"
with ui.nav_panel(None, value="panel5"):
"Panel 5 content"
with ui.nav_panel(None, value="panel6"):
"Panel 6 content"


@reactive.effect
@reactive.event(input.controller1)
def _():
ui.update_navs("hidden_tabs1", selected="panel" + str(input.controller1()))


@reactive.effect
@reactive.event(input.controller2)
def _():
ui.update_navs("hidden_tabs2", selected="panel" + str(input.controller2()))
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from playwright.sync_api import Page, expect

from shiny.playwright import controller
from shiny.playwright.expect._internal import _expect_nav_to_have_header_footer
from shiny.run import ShinyAppProc


def test_navset_hidden_kitchensink(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

navset_hidden_1 = controller.NavsetHidden(page, "hidden_tabs1")
radio1 = controller.InputRadioButtons(page, "controller1")
navset_hidden_1.expect_value("panel2")
navset_hidden_1._expect_content_text("Panel 2 content")
# assert the DOM structure for hidden_navset with header and footer is preserved
_expect_nav_to_have_header_footer(
navset_hidden_1.get_loc_active_content().locator("..").locator(".."),
"navset_hidden_header1",
"navset_hidden_footer1",
)
# assert header and footer contents
expect(page.locator("#navset_hidden_header1")).to_contain_text(
"Navset_hidden_header"
)
expect(page.locator("#navset_hidden_footer1")).to_contain_text(
"Navset_hidden_footer"
)
radio1.set("1")
navset_hidden_1.expect_value("panel1")
navset_hidden_1._expect_content_text("Panel 1 content")

navset_hidden_2 = controller.NavsetHidden(page, "hidden_tabs2")
navset_hidden_2.expect_value("panel4")
Loading
Loading