Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Breaking changes

* `.expect_inverse()` for Navset controllers in `shiny.playwright.controllers` now requires a `bool` value. To keep behavior the same, use `.expect_inverse(False)`. (#1668)

* `.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)

### New features

### Other changes

* Added `PageNavbar` class to the list of `shiny.playwright.controllers` for testing `ui.page_navbar()`. (#1668)

* Added `.expect_widths()` to `NavsetPillList` in `shiny.playwright.controllers` for testing `ui.navset_pill_list(widths=)`. (#1668)

### Bug fixes

* A few fixes for `ui.Chat()`, including:
Expand Down
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",
]
236 changes: 200 additions & 36 deletions shiny/playwright/controller/_navs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from playwright.sync_api import expect as playwright_expect
from typing_extensions import Literal

from shiny.types import ListOrTuple

from .._types import PatternOrStr, Timeout
from ..expect import 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 @@ -79,8 +82,8 @@ def expect_placement(
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
ex_class = "card-header" if location == "above" else "card-footer"
playwright_expect(self.loc_container.locator("..")).to_have_class(
ex_class, timeout=timeout
expect_to_have_class(
self.loc_container.locator(".."), ex_class, timeout=timeout
)


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

def expect_well(self, has_well: bool, *, timeout: Timeout = None) -> None:
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:
playwright_expect(self.loc_container.locator("..")).to_have_class("well")
else:
playwright_expect(self.loc_container.locator("..")).not_to_have_class(
"well"
_expect_class_to_have_value(
self.loc_container.locator(".."), "well", has_class=value, timeout=timeout
)

def expect_widths(
self, value: ListOrTuple[int], *, timeout: Timeout = None
) -> None:
"""
Expects the navset pill list to have the specified widths.

Parameters
----------
value
The expected widths of the navset pill list.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
widths = tuple(value)
assert len(widths) == 2, "`value=` must be a tuple of two integers"
assert all(
isinstance(width, int) for width in widths
), "`value=` must be integers"

loc_row_container = self.loc_container.locator("..").locator("..")

# Make sure the two children are present
loc_complicated = loc_row_container.locator(
"xpath=.",
has=self.page.locator(f"> div.col-sm-{widths[0]} + div.col-sm-{widths[1]}"),
)

# Make sure there are only two children present
try:
playwright_expect(loc_complicated.locator("> div")).to_have_count(
2, timeout=timeout
)
except AssertionError as e:
# Make sure there are only two children
playwright_expect(loc_row_container.locator("> div")).to_have_count(
2, timeout=1
)

expect_to_have_class(
loc_row_container.locator("> div").first,
f"col-sm-{widths[0]}",
timeout=1,
)
expect_to_have_class(
loc_row_container.locator("> div").last,
f"col-sm-{widths[1]}",
timeout=1,
)

# Re-raise the original exception if nothing could be debugged
raise e


class _NavsetCardBase(
Expand Down Expand Up @@ -563,12 +616,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 @@ -627,24 +680,32 @@ def expect_position(
timeout=timeout,
)
else:
playwright_expect(self._loc_navbar).to_have_class(
re.compile(rf"{position}"), timeout=timeout
)
expect_to_have_class(self._loc_navbar, 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 +716,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 +734,128 @@ 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, value: bool, *, timeout: Timeout = None) -> None:
"""
Expects the main content area to be considered a fillable (i.e., flexbox) container

Parameters
----------
value
`True` if the main content area is expected to be fillable, `False` otherwise.
timeout
The maximum time to wait for the expectation to pass. Defaults to `None`.
"""
# confirm page is fillable
_expect_class_to_have_value(
self.page.locator("body"),
"bslib-page-fill",
has_class=value,
timeout=timeout,
)

# confirm content is fillable
_expect_class_to_have_value(
self.get_loc_active_content(),
"html-fill-container",
has_class=value,
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
This method will always call `.expect_fillable(True)` first to ensure the fillable property is set

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
self.expect_fillable(True, timeout=timeout)
_expect_class_to_have_value(
self.page.locator("body"),
"bslib-flow-mobile",
has_class=not value,
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)
Loading
Loading