Skip to content

Commit b16d1a3

Browse files
tests(navs): Add PageNavbar and NavsetHidden tests and controllers (#1668)
Co-authored-by: Barret Schloerke <barret@posit.co>
1 parent 5454eb0 commit b16d1a3

File tree

22 files changed

+641
-82
lines changed

22 files changed

+641
-82
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10+
### Breaking changes
11+
12+
* `.expect_inverse()` for Navset controllers in `shiny.playwright.controllers` now requires a `bool` value. To keep behavior the same, use `.expect_inverse(False)`. (#1668)
13+
14+
* `.expect_layout()` for Navset controllers in `shiny.playwright.controllers` is now renamed to `.expect_fluid()` and requires a `bool` value. To keep behavior the same, use `.expect_fluid(True)` (#1668)
15+
16+
### New features
17+
18+
### Other changes
19+
20+
* Added `PageNavbar` class to the list of `shiny.playwright.controllers` for testing `ui.page_navbar()`. (#1668)
21+
22+
* Added `.expect_widths()` to `NavsetPillList` in `shiny.playwright.controllers` for testing `ui.navset_pill_list(widths=)`. (#1668)
23+
1024
### Bug fixes
1125

1226
* A few fixes for `ui.Chat()`, including:

shiny/playwright/controller/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
NavsetPillList,
6262
NavsetTab,
6363
NavsetUnderline,
64+
PageNavbar,
6465
)
6566
from ._output import (
6667
OutputCode,
@@ -121,4 +122,5 @@
121122
"NavsetUnderline",
122123
"DownloadButton",
123124
"DownloadLink",
125+
"PageNavbar",
124126
]

shiny/playwright/controller/_navs.py

Lines changed: 200 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
from playwright.sync_api import expect as playwright_expect
77
from typing_extensions import Literal
88

9+
from shiny.types import ListOrTuple
10+
911
from .._types import PatternOrStr, Timeout
12+
from ..expect import expect_to_have_class, expect_to_have_style
13+
from ..expect._internal import expect_attribute_to_have_value
1014
from ..expect._internal import expect_class_to_have_value as _expect_class_to_have_value
11-
from ..expect._internal import expect_style_to_have_value as _expect_style_to_have_value
1215
from ._base import (
1316
InitLocator,
1417
UiWithContainer,
@@ -79,8 +82,8 @@ def expect_placement(
7982
The maximum time to wait for the expectation to pass. Defaults to `None`.
8083
"""
8184
ex_class = "card-header" if location == "above" else "card-footer"
82-
playwright_expect(self.loc_container.locator("..")).to_have_class(
83-
ex_class, timeout=timeout
85+
expect_to_have_class(
86+
self.loc_container.locator(".."), ex_class, timeout=timeout
8487
)
8588

8689

@@ -424,23 +427,73 @@ def __init__(self, page: Page, id: str) -> None:
424427
loc="> li.nav-item",
425428
)
426429

427-
def expect_well(self, has_well: bool, *, timeout: Timeout = None) -> None:
430+
def expect_well(self, value: bool, *, timeout: Timeout = None) -> None:
428431
"""
429432
Expects the navset pill list to have a well.
430433
431434
Parameters
432435
----------
433-
has_well
434-
`True` if the navset pill list is expected to have a well, `False` otherwise.
436+
value
437+
`True` if the navset pill list is expected to be constructed with a well,
438+
`False` otherwise.
435439
timeout
436440
The maximum time to wait for the expectation to pass. Defaults to `None`.
437441
"""
438-
if has_well:
439-
playwright_expect(self.loc_container.locator("..")).to_have_class("well")
440-
else:
441-
playwright_expect(self.loc_container.locator("..")).not_to_have_class(
442-
"well"
442+
_expect_class_to_have_value(
443+
self.loc_container.locator(".."), "well", has_class=value, timeout=timeout
444+
)
445+
446+
def expect_widths(
447+
self, value: ListOrTuple[int], *, timeout: Timeout = None
448+
) -> None:
449+
"""
450+
Expects the navset pill list to have the specified widths.
451+
452+
Parameters
453+
----------
454+
value
455+
The expected widths of the navset pill list.
456+
timeout
457+
The maximum time to wait for the expectation to pass. Defaults to `None`.
458+
"""
459+
widths = tuple(value)
460+
assert len(widths) == 2, "`value=` must be a tuple of two integers"
461+
assert all(
462+
isinstance(width, int) for width in widths
463+
), "`value=` must be integers"
464+
465+
loc_row_container = self.loc_container.locator("..").locator("..")
466+
467+
# Make sure the two children are present
468+
loc_complicated = loc_row_container.locator(
469+
"xpath=.",
470+
has=self.page.locator(f"> div.col-sm-{widths[0]} + div.col-sm-{widths[1]}"),
471+
)
472+
473+
# Make sure there are only two children present
474+
try:
475+
playwright_expect(loc_complicated.locator("> div")).to_have_count(
476+
2, timeout=timeout
443477
)
478+
except AssertionError as e:
479+
# Make sure there are only two children
480+
playwright_expect(loc_row_container.locator("> div")).to_have_count(
481+
2, timeout=1
482+
)
483+
484+
expect_to_have_class(
485+
loc_row_container.locator("> div").first,
486+
f"col-sm-{widths[0]}",
487+
timeout=1,
488+
)
489+
expect_to_have_class(
490+
loc_row_container.locator("> div").last,
491+
f"col-sm-{widths[1]}",
492+
timeout=1,
493+
)
494+
495+
# Re-raise the original exception if nothing could be debugged
496+
raise e
444497

445498

446499
class _NavsetCardBase(
@@ -563,12 +616,12 @@ def __init__(self, page: Page, id: str) -> None:
563616
)
564617

565618

566-
class NavsetBar(
619+
class _NavsetBarBase(
567620
_ExpectNavsetSidebarM,
568621
_ExpectNavsetTitleM,
569622
_NavsetBase,
570623
):
571-
"""Controller for :func:`shiny.ui.navset_bar`."""
624+
"""Mixin class for common expectations of nav bars."""
572625

573626
def __init__(self, page: Page, id: str) -> None:
574627
"""
@@ -627,24 +680,32 @@ def expect_position(
627680
timeout=timeout,
628681
)
629682
else:
630-
playwright_expect(self._loc_navbar).to_have_class(
631-
re.compile(rf"{position}"), timeout=timeout
632-
)
683+
expect_to_have_class(self._loc_navbar, position, timeout=timeout)
633684

634-
def expect_inverse(self, *, timeout: Timeout = None) -> None:
685+
def expect_inverse(
686+
self,
687+
value: bool,
688+
*,
689+
timeout: Timeout = None,
690+
) -> None:
635691
"""
636692
Expects the navset bar to be light text color if inverse is True
637693
638694
Parameters
639695
----------
696+
value
697+
`True` if the navset bar is expected to have inverse text color, `False` otherwise.
640698
timeout
641699
The maximum time to wait for the expectation to pass. Defaults to `None`.
642700
"""
643-
playwright_expect(self._loc_navbar).to_have_class(
644-
re.compile("navbar-inverse"), timeout=timeout
701+
_expect_class_to_have_value(
702+
self._loc_navbar,
703+
"navbar-inverse",
704+
has_class=value,
705+
timeout=timeout,
645706
)
646707

647-
def expect_bg(self, bg: str, *, timeout: Timeout = None) -> None:
708+
def expect_bg(self, bg: PatternOrStr, *, timeout: Timeout = None) -> None:
648709
"""
649710
Expects the navset bar to have the specified background color.
650711
@@ -655,11 +716,14 @@ def expect_bg(self, bg: str, *, timeout: Timeout = None) -> None:
655716
timeout
656717
The maximum time to wait for the expectation to pass. Defaults to `None`.
657718
"""
658-
_expect_style_to_have_value(
659-
self._loc_navbar, "background-color", f"{bg} !important", timeout=timeout
719+
expect_to_have_style(
720+
self._loc_navbar,
721+
"background-color",
722+
f"{bg} !important",
723+
timeout=timeout,
660724
)
661725

662-
def expect_gap(self, gap: str, *, timeout: Timeout = None) -> None:
726+
def expect_gap(self, gap: PatternOrStr, *, timeout: Timeout = None) -> None:
663727
"""
664728
Expects the navset bar to have the specified gap.
665729
@@ -670,28 +734,128 @@ def expect_gap(self, gap: str, *, timeout: Timeout = None) -> None:
670734
timeout
671735
The maximum time to wait for the expectation to pass. Defaults to `None`.
672736
"""
673-
_expect_style_to_have_value(
674-
self.get_loc_active_content(), "gap", gap, timeout=timeout
737+
expect_to_have_style(
738+
self.get_loc_active_content(),
739+
"gap",
740+
gap,
741+
timeout=timeout,
675742
)
676743

677-
def expect_layout(
678-
self, layout: Literal["fluid", "fixed"] = "fluid", *, timeout: Timeout = None
744+
def expect_fluid(
745+
self,
746+
value: bool,
747+
*,
748+
timeout: Timeout = None,
679749
) -> None:
680750
"""
681-
Expects the navset bar to have the specified layout.
751+
Expects the navset bar to have a fluid or fixed layout.
682752
683753
Parameters
684754
----------
685-
layout
686-
The expected layout.
755+
value
756+
`True` if the layout is `fluid` or `False` if it is `fixed`.
687757
timeout
688758
The maximum time to wait for the expectation to pass. Defaults to `None`.
689759
"""
690-
if layout == "fluid":
691-
playwright_expect(
692-
self.loc_container.locator("..").locator("..")
693-
).to_have_class(re.compile("container-fluid"), timeout=timeout)
760+
if value:
761+
expect_to_have_class(
762+
self._loc_navbar.locator("> div"),
763+
"container-fluid",
764+
timeout=timeout,
765+
)
694766
else:
695-
playwright_expect(self.loc_container.locator("..")).to_have_class(
696-
re.compile("container"), timeout=timeout
767+
expect_to_have_class(
768+
self._loc_navbar.locator("> div"),
769+
"container",
770+
timeout=timeout,
697771
)
772+
773+
774+
class NavsetBar(_NavsetBarBase):
775+
"""Controller for :func:`shiny.ui.navset_bar`."""
776+
777+
778+
class PageNavbar(_NavsetBarBase):
779+
"""Controller for :func:`shiny.ui.page_navbar`."""
780+
781+
def expect_fillable(self, value: bool, *, timeout: Timeout = None) -> None:
782+
"""
783+
Expects the main content area to be considered a fillable (i.e., flexbox) container
784+
785+
Parameters
786+
----------
787+
value
788+
`True` if the main content area is expected to be fillable, `False` otherwise.
789+
timeout
790+
The maximum time to wait for the expectation to pass. Defaults to `None`.
791+
"""
792+
# confirm page is fillable
793+
_expect_class_to_have_value(
794+
self.page.locator("body"),
795+
"bslib-page-fill",
796+
has_class=value,
797+
timeout=timeout,
798+
)
799+
800+
# confirm content is fillable
801+
_expect_class_to_have_value(
802+
self.get_loc_active_content(),
803+
"html-fill-container",
804+
has_class=value,
805+
timeout=timeout,
806+
)
807+
808+
def expect_fillable_mobile(self, value: bool, *, timeout: Timeout = None) -> None:
809+
"""
810+
Expects the main content area to be considered a fillable (i.e., flexbox) container on mobile
811+
This method will always call `.expect_fillable(True)` first to ensure the fillable property is set
812+
813+
Parameters
814+
----------
815+
value
816+
`True` if the main content area is expected to be fillable on mobile, `False` otherwise.
817+
timeout
818+
The maximum time to wait for the expectation to pass. Defaults to `None`.
819+
"""
820+
821+
# This is important since fillable_mobile needs fillable property to be True
822+
self.expect_fillable(True, timeout=timeout)
823+
_expect_class_to_have_value(
824+
self.page.locator("body"),
825+
"bslib-flow-mobile",
826+
has_class=not value,
827+
timeout=timeout,
828+
)
829+
830+
def expect_window_title(
831+
self, title: PatternOrStr, *, timeout: Timeout = None
832+
) -> None:
833+
"""
834+
Expects the window title to have the specified text.
835+
836+
Parameters
837+
----------
838+
title
839+
The expected window title.
840+
timeout
841+
The maximum time to wait for the expectation to pass. Defaults to `None`.
842+
"""
843+
playwright_expect(self.page).to_have_title(title, timeout=timeout)
844+
845+
def expect_lang(self, lang: PatternOrStr, *, timeout: Timeout = None) -> None:
846+
"""
847+
Expects the HTML tag to have the specified language.
848+
849+
Parameters
850+
----------
851+
lang
852+
The expected language.
853+
timeout
854+
The maximum time to wait for the expectation to pass. Defaults to `None`.
855+
"""
856+
expect_attribute_to_have_value(
857+
self.page.locator("html"),
858+
"lang",
859+
lang,
860+
timeout=timeout,
861+
)

shiny/playwright/expect/_internal.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,41 @@ def expect_class_to_have_value(
9393
expect_to_have_class(loc, class_, timeout=timeout)
9494
else:
9595
expect_not_to_have_class(loc, class_, timeout=timeout)
96+
97+
98+
def _expect_nav_to_have_header_footer(
99+
parent_loc: Locator,
100+
header_id: str,
101+
footer_id: str,
102+
*,
103+
timeout: Timeout = None,
104+
) -> None:
105+
"""
106+
Expect the DOM structure for a header and footer to be preserved.
107+
108+
Parameters
109+
----------
110+
parent_loc
111+
The parent locator to check.
112+
header_id
113+
The ID of the header element.
114+
footer_id
115+
The ID of the footer element.
116+
timeout
117+
The maximum time to wait for the header and footer to appear.
118+
"""
119+
# assert the DOM structure for page_navbar with header and footer is preserved
120+
class_attr = parent_loc.get_attribute("class")
121+
if class_attr and "card" in class_attr:
122+
complicated_parent_loc = parent_loc.locator(
123+
"xpath=.",
124+
has=parent_loc.locator("..").locator(
125+
f".card-body:has(#{header_id}) + .card-body:has(.tab-content) + .card-body #{footer_id}"
126+
),
127+
)
128+
else:
129+
complicated_parent_loc = parent_loc.locator(
130+
"xpath=.",
131+
has=parent_loc.locator(f"#{header_id} + .tab-content + #{footer_id}"),
132+
).locator("..")
133+
playwright_expect(complicated_parent_loc).to_have_count(1, timeout=timeout)

0 commit comments

Comments
 (0)