diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 008476ba5..6108b3055 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -67,7 +67,7 @@ jobs: - name: Run End-to-End tests timeout-minutes: 20 run: | - make playwright-shiny + make playwright-shiny SUB_FILE=". -vv" playwright-examples: runs-on: ${{ matrix.os }} @@ -99,15 +99,15 @@ jobs: - name: Run example app tests timeout-minutes: 20 run: | - make playwright-examples + make playwright-examples SUB_FILE=". -vv" playwright-deploys: # Only allow one `playwright-deploys` job to run at a time. (Independent of branch / PR) concurrency: playwright-deploys runs-on: ${{ matrix.os }} - if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'deploy')) }} strategy: matrix: + # Matches deploy server python version python-version: ["3.10"] os: [ubuntu-latest] fail-fast: false @@ -119,16 +119,27 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Run tests for deploys + - name: Test deploys example apps locally + timeout-minutes: 5 env: + DEPLOY_APPS: "false" + run: | + make playwright-deploys SUB_FILE=". -vv" + + - name: Deploy apps and run tests (on `push` or on `pull_request` w/ `deploy**` branch) + if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'deploy')) }} + env: + DEPLOY_APPS: "true" DEPLOY_CONNECT_SERVER_URL: "https://rsc.radixu.com/" DEPLOY_CONNECT_SERVER_API_KEY: "${{ secrets.DEPLOY_CONNECT_SERVER_API_KEY }}" DEPLOY_SHINYAPPS_NAME: "${{ secrets.DEPLOY_SHINYAPPS_NAME }}" DEPLOY_SHINYAPPS_TOKEN: "${{ secrets.DEPLOY_SHINYAPPS_TOKEN }}" DEPLOY_SHINYAPPS_SECRET: "${{ secrets.DEPLOY_SHINYAPPS_SECRET }}" timeout-minutes: 30 + # Given we are waiting for external servers to finish, + # we can have many local processes waiting for deployment to finish run: | - make playwright-deploys + make playwright-deploys SUB_FILE=". -vv --numprocesses 12" pypi: name: "Deploy to PyPI" diff --git a/.gitignore b/.gitignore index be2b67dd4..70749a9e4 100644 --- a/.gitignore +++ b/.gitignore @@ -113,4 +113,4 @@ docs/source/reference/ # Developer scratch area _dev/ -tests/playwright/deploys/apps/*/requirements.txt +tests/playwright/deploys/**/requirements.txt diff --git a/Makefile b/Makefile index 7f6220dec..d3a4ccc25 100644 --- a/Makefile +++ b/Makefile @@ -86,8 +86,6 @@ test: ## run tests quickly with the default Python # Default `SUB_FILE` to empty SUB_FILE:= -DEPLOYS_FILE:=tests/playwright/deploys - install-playwright: playwright install --with-deps @@ -100,12 +98,12 @@ install-rsconnect: ## install the main version of rsconnect till pypi version su playwright-shiny: install-playwright ## end-to-end tests with playwright pytest tests/playwright/shiny/$(SUB_FILE) +playwright-deploys: install-playwright install-rsconnect ## end-to-end tests on examples with playwright + pytest tests/playwright/deploys/$(SUB_FILE) + playwright-examples: install-playwright ## end-to-end tests on examples with playwright pytest tests/playwright/examples/$(SUB_FILE) -playwright-deploys: install-playwright install-rsconnect ## end-to-end tests on deploys with playwright - pytest tests/playwright/deploys/$(SUB_FILE) -s - testrail-junit: install-playwright install-trcli ## end-to-end tests with playwright and generate junit report pytest tests/playwright/shiny/$(SUB_FILE) --junitxml=report.xml diff --git a/pyrightconfig.json b/pyrightconfig.json index 284b67df5..212089466 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -8,12 +8,12 @@ "sandbox", "_dev", "docs", - "tests/playwright/deploys/apps", + "tests/playwright/deploys/*/app.py", "shiny/templates" ], "typeCheckingMode": "strict", "reportImportCycles": "none", "reportUnusedFunction": "none", "reportPrivateUsage": "none", - "reportUnnecessaryIsInstance": "none", + "reportUnnecessaryIsInstance": "none" } diff --git a/pytest.ini b/pytest.ini index bb3b64ed8..f7b1d8bad 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,8 @@ [pytest] asyncio_mode=strict testpaths=tests/pytest/ +; ; Debug version of options +; addopts = --strict-markers --durations=6 --durations-min=5.0 --browser chromium --numprocesses auto --video=retain-on-failure -vv addopts = --strict-markers --durations=6 --durations-min=5.0 --browser webkit --browser firefox --browser chromium --numprocesses auto markers = examples: Suite of tests to validate that examples do not produce errors (deselect with '-m "not examples"') diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py index 3554b6595..82882e0b5 100644 --- a/tests/playwright/conftest.py +++ b/tests/playwright/conftest.py @@ -11,7 +11,18 @@ from pathlib import PurePath from time import sleep from types import TracebackType -from typing import IO, Any, Callable, Generator, List, Optional, TextIO, Type, Union +from typing import ( + IO, + Any, + Callable, + Generator, + List, + Literal, + Optional, + TextIO, + Type, + Union, +) import pytest @@ -176,25 +187,37 @@ def run_shiny_app( return sa -def create_app_fixture(app: Union[PurePath, str], scope: str = "module"): +def local_app_fixture_gen(app: PurePath | str): + sa = run_shiny_app(app, wait_for_start=False) + try: + with sa: + sa.wait_until_ready(30) + yield sa + finally: + logging.warning("Application output:\n" + str(sa.stderr)) + + +ScopeName = Literal["session", "package", "module", "class", "function"] + + +def create_app_fixture( + app: Union[PurePath, str], + scope: ScopeName = "module", +): + @pytest.fixture(scope=scope) def fixture_func(): - sa = run_shiny_app(app, wait_for_start=False) - try: - with sa: - sa.wait_until_ready(30) - yield sa - finally: - logging.warning("Application output:\n" + str(sa.stderr)) + # Pass through `yield` via `next(...)` call + # (`yield` must be on same line as `next`!) + app_gen = local_app_fixture_gen(app) + yield next(app_gen) - return pytest.fixture( - scope=scope, # type: ignore - )(fixture_func) + return fixture_func def create_example_fixture( example_name: str, example_file: str = "app.py", - scope: str = "module", + scope: ScopeName = "module", ): """Used to create app fixtures from apps in py-shiny/examples""" return create_app_fixture( @@ -205,7 +228,7 @@ def create_example_fixture( def create_doc_example_fixture( example_name: str, example_file: str = "app.py", - scope: str = "module", + scope: ScopeName = "module", ): """Used to create app fixtures from apps in py-shiny/shiny/api-examples""" return create_app_fixture( @@ -215,7 +238,7 @@ def create_doc_example_fixture( def create_doc_example_core_fixture( example_name: str, - scope: str = "module", + scope: ScopeName = "module", ): """Used to create app fixtures from ``app-core.py`` example apps in py-shiny/shiny/api-examples""" return create_doc_example_fixture(example_name, "app-core.py", scope) @@ -223,20 +246,13 @@ def create_doc_example_core_fixture( def create_doc_example_express_fixture( example_name: str, - scope: str = "module", + scope: ScopeName = "module", ): """Used to create app fixtures from ``app-express.py`` example apps in py-shiny/shiny/api-examples""" return create_doc_example_fixture(example_name, "app-express.py", scope) -def create_deploys_fixture(app: Union[PurePath, str], scope: str = "module"): - """Used to create app fixtures from apps in tests/playwright/deploys/apps""" - return create_app_fixture( - here_root / "tests/playwright/deploys/apps" / app / "app.py", scope - ) - - -def x_create_doc_example_fixture(example_name: str, scope: str = "module"): +def x_create_doc_example_fixture(example_name: str, scope: ScopeName = "module"): """Used to create app fixtures from apps in py-shiny/shiny/examples""" return create_app_fixture( here_root / "shiny/experimental/api-examples" / example_name / "app.py", scope @@ -245,13 +261,8 @@ def x_create_doc_example_fixture(example_name: str, scope: str = "module"): @pytest.fixture(scope="module") def local_app(request: pytest.FixtureRequest) -> Generator[ShinyAppProc, None, None]: - sa = run_shiny_app(PurePath(request.path).parent / "app.py", wait_for_start=False) - try: - with sa: - sa.wait_until_ready(30) - yield sa - finally: - logging.warning("Application output:\n" + str(sa.stderr)) + app_gen = local_app_fixture_gen(PurePath(request.path).parent / "app.py") + yield next(app_gen) @contextmanager diff --git a/tests/playwright/controls.py b/tests/playwright/controls.py index 92f8906c1..4e4ad4217 100644 --- a/tests/playwright/controls.py +++ b/tests/playwright/controls.py @@ -2170,7 +2170,21 @@ def __init__( super().__init__(page, id=id, loc=f"#{id}.shiny-text-output") -# TODO-Karan: Add OutputCode class +class OutputCode(_OutputTextValue): + def __init__(self, page: Page, id: str) -> None: + super().__init__(page, id=id, loc=f"pre#{id}.shiny-text-output") + + def expect_has_placeholder( + self, placeholder: bool = False, *, timeout: Timeout = None + ) -> None: + _expect_class_value( + self.loc, + cls="noplaceholder", + has_class=not placeholder, + timeout=timeout, + ) + + class OutputTextVerbatim(_OutputTextValue): def __init__(self, page: Page, id: str) -> None: super().__init__(page, id=id, loc=f"pre#{id}.shiny-text-output") @@ -2826,18 +2840,30 @@ def _get_overlay_id(self, *, timeout: Timeout = None) -> str | None: loc_el.scroll_into_view_if_needed(timeout=timeout) return loc_el.get_attribute("aria-describedby") - @property - def loc_overlay_body(self) -> Locator: + # @property + # def loc_overlay_body(self) -> Locator: + # # Can not leverage `self.loc_overlay_container` as `self._overlay_selector` must + # # be concatenated directly to the result of `self._get_overlay_id()` + # return self.page.locator(f"#{self._get_overlay_id()}{self._overlay_selector}") + + # @property + # def loc_overlay_container(self) -> Locator: + # return self.page.locator(f"#{self._get_overlay_id()}") + + def get_loc_overlay_body(self, *, timeout: Timeout = None) -> Locator: # Can not leverage `self.loc_overlay_container` as `self._overlay_selector` must # be concatenated directly to the result of `self._get_overlay_id()` - return self.page.locator(f"#{self._get_overlay_id()}{self._overlay_selector}") + return self.page.locator( + f"#{self._get_overlay_id(timeout=timeout)}{self._overlay_selector}" + ) - @property - def loc_overlay_container(self) -> Locator: - return self.page.locator(f"#{self._get_overlay_id()}") + def get_loc_overlay_container(self, *, timeout: Timeout = None) -> Locator: + return self.page.locator(f"#{self._get_overlay_id(timeout=timeout)}") def expect_body(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: - playwright_expect(self.loc_overlay_body).to_have_text(value, timeout=timeout) + playwright_expect(self.get_loc_overlay_body(timeout=timeout)).to_have_text( + value, timeout=timeout + ) def expect_active(self, active: bool, *, timeout: Timeout = None) -> None: value = re.compile(r".*") if active else None @@ -2850,7 +2876,7 @@ def expect_active(self, active: bool, *, timeout: Timeout = None) -> None: def expect_placement(self, value: str, *, timeout: Timeout = None) -> None: return expect_attr( - loc=self.loc_overlay_container, + loc=self.get_loc_overlay_container(timeout=timeout), timeout=timeout, name="data-popper-placement", value=value, @@ -2875,7 +2901,7 @@ def __init__(self, page: Page, id: str) -> None: ) def set(self, open: bool, timeout: Timeout = None) -> None: - if open ^ self.loc_overlay_body.count() > 0: + if open ^ self.get_loc_overlay_body(timeout=timeout).count() > 0: self.toggle() def toggle(self, timeout: Timeout = None) -> None: @@ -2901,10 +2927,10 @@ def __init__(self, page: Page, id: str) -> None: ) def set(self, open: bool, timeout: Timeout = None) -> None: - if open ^ self.loc_overlay_body.count() > 0: + if open ^ self.get_loc_overlay_body(timeout=timeout).count() > 0: self.toggle(timeout=timeout) if not open: - self.loc_overlay_body.click() + self.get_loc_overlay_body(timeout=timeout).click() def toggle(self, timeout: Timeout = None) -> None: self.loc_trigger.wait_for(state="visible", timeout=timeout) @@ -2928,17 +2954,25 @@ def expect_value(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: self.loc_container.locator('a[role="tab"].active') ).to_have_attribute("data-value", value, timeout=timeout) - # TODO-future: Make it a single locator expectation - # get active content instead of assertion - @property - def loc_active_content(self) -> Locator: - datatab_id = self.loc_container.get_attribute("data-tabsetid") + # # TODO-future: Make it a single locator expectation + # # get active content instead of assertion + # @property + # def loc_active_content(self) -> Locator: + # datatab_id = self.loc_container.get_attribute("data-tabsetid") + # return self.page.locator( + # f"div.tab-content[data-tabsetid='{datatab_id}'] > div.tab-pane.active" + # ) + + def get_loc_active_content(self, *, timeout: Timeout = None) -> Locator: + datatab_id = self.loc_container.get_attribute("data-tabsetid", timeout=timeout) return self.page.locator( f"div.tab-content[data-tabsetid='{datatab_id}'] > div.tab-pane.active" ) def expect_content(self, value: PatternOrStr, *, timeout: Timeout = None) -> None: - playwright_expect(self.loc_active_content).to_have_text(value, timeout=timeout) + playwright_expect(self.get_loc_active_content()).to_have_text( + value, timeout=timeout + ) def expect_nav_values( self, diff --git a/tests/playwright/deploys/apps/__init__.py b/tests/playwright/deploys/apps/__init__.py deleted file mode 100644 index 8a3b86141..000000000 --- a/tests/playwright/deploys/apps/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Deploy package used to test deploys""" diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt deleted file mode 100644 index b20ad55dd..000000000 --- a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools -fastapi==0.108.0 diff --git a/tests/playwright/shiny/shiny-express/accordion/__init__.py b/tests/playwright/deploys/express-accordion/__init__.py similarity index 100% rename from tests/playwright/shiny/shiny-express/accordion/__init__.py rename to tests/playwright/deploys/express-accordion/__init__.py diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/app.py b/tests/playwright/deploys/express-accordion/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-accordion/app.py rename to tests/playwright/deploys/express-accordion/app.py diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt b/tests/playwright/deploys/express-accordion/app_requirements.txt similarity index 79% rename from tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt rename to tests/playwright/deploys/express-accordion/app_requirements.txt index b20ad55dd..03309f726 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fillable/app_requirements.txt +++ b/tests/playwright/deploys/express-accordion/app_requirements.txt @@ -1,2 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools -fastapi==0.108.0 + diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/rsconnect-python/shiny-express-accordion.json b/tests/playwright/deploys/express-accordion/rsconnect-python/express-accordion.json similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-accordion/rsconnect-python/shiny-express-accordion.json rename to tests/playwright/deploys/express-accordion/rsconnect-python/express-accordion.json diff --git a/tests/playwright/deploys/express-accordion/test_accordion.py b/tests/playwright/deploys/express-accordion/test_accordion.py new file mode 100644 index 000000000..a13aeca0b --- /dev/null +++ b/tests/playwright/deploys/express-accordion/test_accordion.py @@ -0,0 +1,17 @@ +from controls import Accordion +from playwright.sync_api import Page +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +app_url = create_deploys_app_url_fixture("shiny_express_accordion") + + +@skip_if_not_chrome +def test_express_accordion(page: Page, app_url: str) -> None: + page.goto(app_url) + + acc = Accordion(page, "express_accordion") + acc_panel_2 = acc.accordion_panel("Panel 2") + acc_panel_2.expect_open(True) + acc_panel_2.expect_body("n = 50") + acc_panel_2.set(False) + acc_panel_2.expect_open(False) diff --git a/tests/playwright/deploys/apps/shiny-express-dataframe/app.py b/tests/playwright/deploys/express-dataframe/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-dataframe/app.py rename to tests/playwright/deploys/express-dataframe/app.py diff --git a/tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt b/tests/playwright/deploys/express-dataframe/app_requirements.txt similarity index 80% rename from tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt rename to tests/playwright/deploys/express-dataframe/app_requirements.txt index 2399add11..802c79e6c 100644 --- a/tests/playwright/deploys/apps/shiny-express-dataframe/app_requirements.txt +++ b/tests/playwright/deploys/express-dataframe/app_requirements.txt @@ -1,3 +1,3 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools pandas -fastapi==0.108.0 + diff --git a/tests/playwright/deploys/apps/shiny-express-dataframe/rsconnect-python/shiny-express-dataframe.json b/tests/playwright/deploys/express-dataframe/rsconnect-python/express-dataframe.json similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-dataframe/rsconnect-python/shiny-express-dataframe.json rename to tests/playwright/deploys/express-dataframe/rsconnect-python/express-dataframe.json diff --git a/tests/playwright/deploys/express-dataframe/test_dataframe.py b/tests/playwright/deploys/express-dataframe/test_dataframe.py new file mode 100644 index 000000000..fc93b86dc --- /dev/null +++ b/tests/playwright/deploys/express-dataframe/test_dataframe.py @@ -0,0 +1,13 @@ +from controls import OutputDataFrame +from playwright.sync_api import Page +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +app_url = create_deploys_app_url_fixture("shiny-express-dataframe") + + +@skip_if_not_chrome +def test_express_dataframe_deploys(page: Page, app_url: str) -> None: + page.goto(app_url) + + dataframe = OutputDataFrame(page, "sample_data_frame") + dataframe.expect_n_row(6) diff --git a/tests/playwright/deploys/apps/shiny-express-folium/app.py b/tests/playwright/deploys/express-folium/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-folium/app.py rename to tests/playwright/deploys/express-folium/app.py diff --git a/tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt b/tests/playwright/deploys/express-folium/app_requirements.txt similarity index 80% rename from tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt rename to tests/playwright/deploys/express-folium/app_requirements.txt index 52e2dfa93..27347d1c8 100644 --- a/tests/playwright/deploys/apps/shiny-express-folium/app_requirements.txt +++ b/tests/playwright/deploys/express-folium/app_requirements.txt @@ -1,3 +1,3 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools folium -fastapi==0.108.0 + diff --git a/tests/playwright/deploys/apps/shiny-express-folium/rsconnect-python/shiny-express-folium.json b/tests/playwright/deploys/express-folium/rsconnect-python/express-folium.json similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-folium/rsconnect-python/shiny-express-folium.json rename to tests/playwright/deploys/express-folium/rsconnect-python/express-folium.json diff --git a/tests/playwright/deploys/express-folium/test_folium.py b/tests/playwright/deploys/express-folium/test_folium.py new file mode 100644 index 000000000..5c204af99 --- /dev/null +++ b/tests/playwright/deploys/express-folium/test_folium.py @@ -0,0 +1,22 @@ +from playwright.sync_api import Page, expect +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +app_url = create_deploys_app_url_fixture("shiny-express-folium") + + +@skip_if_not_chrome +def test_folium_map(page: Page, app_url: str) -> None: + page.goto(app_url) + + expect(page.get_by_text("Static Map")).to_have_count(1) + expect(page.get_by_text("Map inside of render express call")).to_have_count(1) + # map inside the @render.express + expect( + page.frame_locator("iframe").nth(1).get_by_role("link", name="OpenStreetMap") + ).to_have_count(1) + # map outside of the @render.express at the top level + expect( + page.frame_locator("iframe") + .nth(0) + .get_by_role("link", name="U.S. Geological Survey") + ).to_have_count(1) diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/app.py b/tests/playwright/deploys/express-page_default/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-default/app.py rename to tests/playwright/deploys/express-page_default/app.py diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt b/tests/playwright/deploys/express-page_default/app_requirements.txt similarity index 79% rename from tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt rename to tests/playwright/deploys/express-page_default/app_requirements.txt index b20ad55dd..03309f726 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-default/app_requirements.txt +++ b/tests/playwright/deploys/express-page_default/app_requirements.txt @@ -1,2 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools -fastapi==0.108.0 + diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/rsconnect-python/shiny-express-page-default.json b/tests/playwright/deploys/express-page_default/rsconnect-python/express-page_default.json similarity index 86% rename from tests/playwright/deploys/apps/shiny-express-page-default/rsconnect-python/shiny-express-page-default.json rename to tests/playwright/deploys/express-page_default/rsconnect-python/express-page_default.json index b4cc965be..b90e38bb4 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-default/rsconnect-python/shiny-express-page-default.json +++ b/tests/playwright/deploys/express-page_default/rsconnect-python/express-page_default.json @@ -4,7 +4,7 @@ "app_url": "https://testing-apps.shinyapps.io/shiny_express_page_default/", "app_id": 10800233, "app_guid": null, - "title": "shiny_express_page_default", + "title": "shiny-express-folium", "app_mode": "python-shiny", "app_store_version": 1 } diff --git a/tests/playwright/deploys/express-page_default/test_page_default.py b/tests/playwright/deploys/express-page_default/test_page_default.py new file mode 100644 index 000000000..2ef290bd1 --- /dev/null +++ b/tests/playwright/deploys/express-page_default/test_page_default.py @@ -0,0 +1,29 @@ +from controls import LayoutNavsetTab +from playwright.sync_api import Page, expect +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +TIMEOUT = 2 * 60 * 1000 + +app_url = create_deploys_app_url_fixture("shiny_express_page_default") + + +@skip_if_not_chrome +def test_page_default(page: Page, app_url: str) -> None: + page.goto(app_url) + + # since it is a custom table we can't use the OutputTable controls + expect(page.locator("#shell")).to_have_text( + "R1C1R1\nR1C1R2-R1C1R1\nR1C1R2-R1C1R2\nR1C1R2-R1C2\nR1C2", timeout=TIMEOUT + ) + + # Perform these tests second as their locators are not stable over time. + # (They require that a locator be realized before finding the second locator) + nav_html = LayoutNavsetTab(page, "express_navset_tab") + nav_html.expect_content("pre 0pre 1pre 2") + nav_html.set("div") + nav_html.expect_content("div 0\ndiv 1\ndiv 2") + nav_html.set("span") + nav_html.expect_content("span 0span 1span 2") + + navset_card_tab = LayoutNavsetTab(page, "express_navset_card_tab") + navset_card_tab.expect_content("") diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py b/tests/playwright/deploys/express-page_fillable/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-fillable/app.py rename to tests/playwright/deploys/express-page_fillable/app.py diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt b/tests/playwright/deploys/express-page_fillable/app_requirements.txt similarity index 79% rename from tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt rename to tests/playwright/deploys/express-page_fillable/app_requirements.txt index b20ad55dd..87a253e09 100644 --- a/tests/playwright/deploys/apps/shiny-express-accordion/app_requirements.txt +++ b/tests/playwright/deploys/express-page_fillable/app_requirements.txt @@ -1,2 +1 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools -fastapi==0.108.0 diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/rsconnect-python/shiny-express-page-fillable.json b/tests/playwright/deploys/express-page_fillable/rsconnect-python/express-page_fillable.json similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-fillable/rsconnect-python/shiny-express-page-fillable.json rename to tests/playwright/deploys/express-page_fillable/rsconnect-python/express-page_fillable.json diff --git a/tests/playwright/deploys/express-page_fillable/test_page_fillable.py b/tests/playwright/deploys/express-page_fillable/test_page_fillable.py new file mode 100644 index 000000000..a1b953976 --- /dev/null +++ b/tests/playwright/deploys/express-page_fillable/test_page_fillable.py @@ -0,0 +1,17 @@ +from controls import Card, OutputTextVerbatim +from playwright.sync_api import Page +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +app_url = create_deploys_app_url_fixture("express_page_fillable") + + +@skip_if_not_chrome +def test_express_page_fillable(page: Page, app_url: str) -> None: + page.goto(app_url) + + card = Card(page, "card") + output_txt = OutputTextVerbatim(page, "txt") + output_txt.expect_value("50") + bounding_box = card.loc.bounding_box() + assert bounding_box is not None + assert bounding_box["height"] > 300 diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py b/tests/playwright/deploys/express-page_fluid/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-fluid/app.py rename to tests/playwright/deploys/express-page_fluid/app.py diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt b/tests/playwright/deploys/express-page_fluid/app_requirements.txt similarity index 79% rename from tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt rename to tests/playwright/deploys/express-page_fluid/app_requirements.txt index b20ad55dd..03309f726 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fluid/app_requirements.txt +++ b/tests/playwright/deploys/express-page_fluid/app_requirements.txt @@ -1,2 +1,2 @@ git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools -fastapi==0.108.0 + diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/rsconnect-python/shiny-express-page-fluid.json b/tests/playwright/deploys/express-page_fluid/rsconnect-python/express-page_fluid.json similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-fluid/rsconnect-python/shiny-express-page-fluid.json rename to tests/playwright/deploys/express-page_fluid/rsconnect-python/express-page_fluid.json diff --git a/tests/playwright/deploys/express-page_fluid/test_page_fluid.py b/tests/playwright/deploys/express-page_fluid/test_page_fluid.py new file mode 100644 index 000000000..93bca30ca --- /dev/null +++ b/tests/playwright/deploys/express-page_fluid/test_page_fluid.py @@ -0,0 +1,17 @@ +from controls import Card, OutputTextVerbatim +from playwright.sync_api import Page +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +app_url = create_deploys_app_url_fixture("express_page_fluid") + + +@skip_if_not_chrome +def test_express_page_fluid(page: Page, app_url: str) -> None: + page.goto(app_url) + + card = Card(page, "card") + output_txt = OutputTextVerbatim(page, "txt") + output_txt.expect_value("50") + bounding_box = card.loc.bounding_box() + assert bounding_box is not None + assert bounding_box["height"] < 300 diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py b/tests/playwright/deploys/express-page_sidebar/app.py similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py rename to tests/playwright/deploys/express-page_sidebar/app.py diff --git a/tests/playwright/deploys/express-page_sidebar/app_requirements.txt b/tests/playwright/deploys/express-page_sidebar/app_requirements.txt new file mode 100644 index 000000000..03309f726 --- /dev/null +++ b/tests/playwright/deploys/express-page_sidebar/app_requirements.txt @@ -0,0 +1,2 @@ +git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools + diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/rsconnect-python/shiny-express-page-sidebar.json b/tests/playwright/deploys/express-page_sidebar/rsconnect-python/express-page_sidebar.json similarity index 100% rename from tests/playwright/deploys/apps/shiny-express-page-sidebar/rsconnect-python/shiny-express-page-sidebar.json rename to tests/playwright/deploys/express-page_sidebar/rsconnect-python/express-page_sidebar.json diff --git a/tests/playwright/deploys/express-page_sidebar/test_page_sidebar.py b/tests/playwright/deploys/express-page_sidebar/test_page_sidebar.py new file mode 100644 index 000000000..39a31c196 --- /dev/null +++ b/tests/playwright/deploys/express-page_sidebar/test_page_sidebar.py @@ -0,0 +1,20 @@ +from controls import OutputTextVerbatim, Sidebar +from playwright.sync_api import Page +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome +from utils.express_utils import compare_annotations + +from shiny import ui +from shiny.express import ui as xui + +app_url = create_deploys_app_url_fixture("express_page_sidebar") + + +@skip_if_not_chrome +def test_express_page_sidebar(page: Page, app_url: str) -> None: + page.goto(app_url) + + sidebar = Sidebar(page, "sidebar") + sidebar.expect_text("SidebarTitle Sidebar Content") + output_txt = OutputTextVerbatim(page, "txt") + output_txt.expect_value("50") + compare_annotations(ui.sidebar, xui.sidebar) diff --git a/tests/playwright/deploys/apps/plotly_app/app.py b/tests/playwright/deploys/plotly/app.py similarity index 98% rename from tests/playwright/deploys/apps/plotly_app/app.py rename to tests/playwright/deploys/plotly/app.py index 25a097526..5f93ea463 100644 --- a/tests/playwright/deploys/apps/plotly_app/app.py +++ b/tests/playwright/deploys/plotly/app.py @@ -59,7 +59,7 @@ def summary_data(): height="100%", ) - @reactive.calc + @reactive.Calc def filtered_df(): # input.summary_data_selected_rows() is a tuple, so we must convert it to list, # as that's what Pandas requires for indexing. @@ -114,7 +114,7 @@ def synchronize_size(output_id): def wrapper(func): input = session.get_current_session().input - @reactive.effect + @reactive.Effect def size_updater(): func( input[f".clientdata_output_{output_id}_width"](), diff --git a/tests/playwright/deploys/apps/plotly_app/app_requirements.txt b/tests/playwright/deploys/plotly/app_requirements.txt similarity index 89% rename from tests/playwright/deploys/apps/plotly_app/app_requirements.txt rename to tests/playwright/deploys/plotly/app_requirements.txt index 5238309ef..b462d0a64 100644 --- a/tests/playwright/deploys/apps/plotly_app/app_requirements.txt +++ b/tests/playwright/deploys/plotly/app_requirements.txt @@ -2,4 +2,4 @@ pandas plotly git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools git+https://github.com/posit-dev/py-shinywidgets.git#egg=shinywidgets -fastapi==0.108.0 + diff --git a/tests/playwright/deploys/apps/plotly_app/rsconnect-python/plotly_app.json b/tests/playwright/deploys/plotly/rsconnect-python/plotly.json similarity index 100% rename from tests/playwright/deploys/apps/plotly_app/rsconnect-python/plotly_app.json rename to tests/playwright/deploys/plotly/rsconnect-python/plotly.json diff --git a/tests/playwright/deploys/plotly/test_plotly_app.py b/tests/playwright/deploys/plotly/test_plotly_app.py new file mode 100644 index 000000000..5d62f456c --- /dev/null +++ b/tests/playwright/deploys/plotly/test_plotly_app.py @@ -0,0 +1,19 @@ +from playwright.sync_api import Page, expect +from utils.deploy_utils import create_deploys_app_url_fixture, skip_if_not_chrome + +TIMEOUT = 2 * 60 * 1000 +app_url = create_deploys_app_url_fixture("example_deploy_app_A") + + +@skip_if_not_chrome +def test_deploys(page: Page, app_url: str) -> None: + page.goto(app_url) + + COUNTRY = "Afghanistan" + expect(page.get_by_text(COUNTRY)).to_have_count(1, timeout=TIMEOUT) + page.get_by_role("cell", name=COUNTRY).click() + expect(page.locator("#country_detail_pop")).to_contain_text( + COUNTRY, timeout=TIMEOUT + ) + expect(page.locator("#country_detail_percap")).to_contain_text(COUNTRY) + expect(page.get_by_text(COUNTRY)).to_have_count(3) diff --git a/tests/playwright/deploys/tests/test_accordion.py b/tests/playwright/deploys/tests/test_accordion.py deleted file mode 100644 index dcfcde2d5..000000000 --- a/tests/playwright/deploys/tests/test_accordion.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_accordion - -APP_DIR = "shiny-express-accordion" -APP_NAME = "shiny_express_accordion" -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_express_accordion(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_accordion(page) diff --git a/tests/playwright/deploys/tests/test_dataframe.py b/tests/playwright/deploys/tests/test_dataframe.py deleted file mode 100644 index 9141f5ccc..000000000 --- a/tests/playwright/deploys/tests/test_dataframe.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_dataframe - -APP_DIR = "shiny-express-dataframe" -APP_NAME = "shiny-express-dataframe" -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_express_dataframe(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_dataframe(page) - - -# TODO-Karan: Add a way to run the deploy tests locally without deploys for the playwright-shiny cmd diff --git a/tests/playwright/deploys/tests/test_folium.py b/tests/playwright/deploys/tests/test_folium.py deleted file mode 100644 index ba5e7d04c..000000000 --- a/tests/playwright/deploys/tests/test_folium.py +++ /dev/null @@ -1,23 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_folium_render - -APP_DIR = "shiny-express-folium" -APP_NAME = "shiny-express-folium" -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_folium_map(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_folium_render(page) diff --git a/tests/playwright/deploys/tests/test_page_default.py b/tests/playwright/deploys/tests/test_page_default.py deleted file mode 100644 index 925a1a815..000000000 --- a/tests/playwright/deploys/tests/test_page_default.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_page_default - -APP_DIR = "shiny-express-page-default" -APP_NAME = "shiny_express_page_default" -# reqd since the app on connect takes a while to load -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 - - -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_express_page_default(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_page_default(page) diff --git a/tests/playwright/deploys/tests/test_page_fillable.py b/tests/playwright/deploys/tests/test_page_fillable.py deleted file mode 100644 index 30854c8a0..000000000 --- a/tests/playwright/deploys/tests/test_page_fillable.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_page_fillable - -APP_DIR = "shiny-express-page-fillable" -APP_NAME = "express_page_fillable" -# reqd since the app on connect takes a while to load -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 - - -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_express_page_fillable(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_page_fillable(page) diff --git a/tests/playwright/deploys/tests/test_page_fluid.py b/tests/playwright/deploys/tests/test_page_fluid.py deleted file mode 100644 index 2da0a94c5..000000000 --- a/tests/playwright/deploys/tests/test_page_fluid.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_page_fluid - -APP_DIR = "shiny-express-page-fluid" -APP_NAME = "express_page_fluid" -# reqd since the app on connect takes a while to load -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 - - -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_express_page_fluid(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_page_fluid(page) diff --git a/tests/playwright/deploys/tests/test_page_sidebar.py b/tests/playwright/deploys/tests/test_page_sidebar.py deleted file mode 100644 index f8a72b6a8..000000000 --- a/tests/playwright/deploys/tests/test_page_sidebar.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page -from utils.deploy_utils import deploy, write_requirements_txt -from utils.express_utils import verify_express_page_sidebar - -APP_DIR = "shiny-express-page-sidebar" -APP_NAME = "express_page_sidebar" -# reqd since the app on connect takes a while to load -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 - - -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_express_page_sidebar(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - verify_express_page_sidebar(page) diff --git a/tests/playwright/deploys/tests/test_plotly_app.py b/tests/playwright/deploys/tests/test_plotly_app.py deleted file mode 100644 index b2c130da9..000000000 --- a/tests/playwright/deploys/tests/test_plotly_app.py +++ /dev/null @@ -1,35 +0,0 @@ -import os - -import pytest -from playwright.sync_api import Page, expect -from utils.deploy_utils import deploy, write_requirements_txt - -COUNTRY = "Afghanistan" -APP_DIR = "plotly_app" -APP_NAME = "example_deploy_app_A" -# reqd since the app on connect takes a while to load -PAGE_TIMEOUT = 120 * 1000 -EXPECT_TIMEOUT = 30 * 1000 - - -current_dir = os.path.dirname(os.path.abspath(__file__)) -app_file_path = os.path.join(os.path.dirname(current_dir), "apps", APP_DIR) - - -@pytest.mark.integrationtest -@pytest.mark.only_browser("chromium") -@pytest.mark.parametrize("location", ["connect", "shinyapps"]) -def test_deploys(page: Page, location: str) -> None: - write_requirements_txt(app_file_path) - page_url = deploy(location, APP_NAME, app_file_path) - page.goto(page_url, timeout=PAGE_TIMEOUT) - - expect(page.get_by_text(COUNTRY)).to_have_count(1, timeout=EXPECT_TIMEOUT) - page.get_by_role("cell", name=COUNTRY).click(timeout=EXPECT_TIMEOUT) - expect(page.locator("#country_detail_pop")).to_contain_text( - COUNTRY, timeout=EXPECT_TIMEOUT - ) - expect(page.locator("#country_detail_percap")).to_contain_text( - COUNTRY, timeout=EXPECT_TIMEOUT - ) - expect(page.get_by_text(COUNTRY)).to_have_count(3, timeout=EXPECT_TIMEOUT) diff --git a/tests/playwright/examples/example_apps.py b/tests/playwright/examples/example_apps.py index a2e56fdd3..3fa4c7ca2 100644 --- a/tests/playwright/examples/example_apps.py +++ b/tests/playwright/examples/example_apps.py @@ -32,17 +32,10 @@ def get_apps(path: str) -> typing.List[str]: return app_paths -example_apps: typing.List[str] = [ - *get_apps("examples"), - *get_apps("shiny/api-examples"), - *get_apps("shiny/templates/app-templates"), - *get_apps("tests/playwright/deploys"), -] - app_idle_wait = {"duration": 300, "timeout": 5 * 1000} app_hard_wait: typing.Dict[str, int] = { - "brownian": 250, - "ui-func": 250, + "examples/brownian": 250, + "examples/ui-func": 250, } output_transformer_errors = [ "ShinyDeprecationWarning: `shiny.render.transformer.output_transformer()`", @@ -56,22 +49,23 @@ def get_apps(path: str) -> typing.List[str]: app_allow_shiny_errors: typing.Dict[ str, typing.Union[Literal[True], typing.List[str]] ] = { - "SafeException": True, - "global_pyplot": True, - "static_plots": [ + "api-examples/SafeException": True, + "examples/global_pyplot": True, + "examples/static_plots": [ # acceptable warning "PlotnineWarning: Smoothing requires 2 or more points", "RuntimeWarning: divide by zero encountered", "UserWarning: This figure includes Axes that are not compatible with tight_layout", ], # Remove after shinywidgets accepts `Renderer` PR - "airmass": [*output_transformer_errors], - "brownian": [*output_transformer_errors], - "multi-page": [*output_transformer_errors], - "model-score": [*output_transformer_errors], - "data_frame": [*output_transformer_errors], - "output_transformer": [*output_transformer_errors], - "render_express": [*express_warnings], + "api-examples/data_frame": [*output_transformer_errors], + "api-examples/output_transformer": [*output_transformer_errors], + "api-examples/render_express": [*express_warnings], + "app-templates/multi-page": [*output_transformer_errors], + "examples/airmass": [*output_transformer_errors], + "examples/brownian": [*output_transformer_errors], + "examples/model-score": [*output_transformer_errors], + "deploys/plotly": [*output_transformer_errors], } app_allow_external_errors: typing.List[str] = [ # TODO-garrick-future: Remove after fixing sidebar max_height_mobile warning @@ -98,7 +92,7 @@ def get_apps(path: str) -> typing.List[str]: "pd.option_context('mode.use_inf_as_na", # continutation of line above ] app_allow_js_errors: typing.Dict[str, typing.List[str]] = { - "brownian": ["Failed to acquire camera feed:"], + "examples/brownian": ["Failed to acquire camera feed:"], } @@ -176,11 +170,12 @@ def on_console_msg(msg: ConsoleMessage) -> None: page.goto(app.url) app_name = os.path.basename(os.path.dirname(ex_app_path)) + short_app_path = f"{os.path.basename(os.path.dirname(os.path.dirname(ex_app_path)))}/{app_name}" - if app_name in app_hard_wait.keys(): + if short_app_path in app_hard_wait.keys(): # Apps are constantly invalidating and will not stabilize # Instead, wait for specific amount of time - page.wait_for_timeout(app_hard_wait[app_name]) + page.wait_for_timeout(app_hard_wait[short_app_path]) else: # Wait for app to stop updating wait_for_idle_app( @@ -200,8 +195,8 @@ def on_console_msg(msg: ConsoleMessage) -> None: ] # Remove any app specific errors that are allowed - if app_name in app_allow_shiny_errors: - app_allowable_errors = app_allow_shiny_errors[app_name] + if short_app_path in app_allow_shiny_errors: + app_allowable_errors = app_allow_shiny_errors[short_app_path] else: app_allowable_errors = [] @@ -226,7 +221,7 @@ def on_console_msg(msg: ConsoleMessage) -> None: and not any([error_txt in line for error_txt in app_allowable_errors]) ] if len(error_lines) > 0: - print("\napp_name: " + app_name) + print("\nshort_app_path: " + short_app_path) print("\napp_allowable_errors :") print("\n".join(app_allowable_errors)) print("\nError lines remaining:") @@ -234,13 +229,16 @@ def on_console_msg(msg: ConsoleMessage) -> None: assert len(error_lines) == 0 # Check for JavaScript errors - if app_name in app_allow_js_errors: + if short_app_path in app_allow_js_errors: # Remove any errors that are allowed console_errors = [ line for line in console_errors if not any( - [error_txt in line for error_txt in app_allow_js_errors[app_name]] + [ + error_txt in line + for error_txt in app_allow_js_errors[short_app_path] + ] ) ] assert len(console_errors) == 0, ( diff --git a/tests/playwright/examples/test_deploy_examples.py b/tests/playwright/examples/test_deploy_examples.py deleted file mode 100644 index 767ad5b98..000000000 --- a/tests/playwright/examples/test_deploy_examples.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest -from example_apps import get_apps, reruns, validate_example -from playwright.sync_api import Page - - -@pytest.mark.flaky(reruns=reruns, reruns_delay=1) -@pytest.mark.parametrize("ex_app_path", get_apps("tests/playwright/deploys")) -def test_deploy_examples(page: Page, ex_app_path: str) -> None: - validate_example(page, ex_app_path) diff --git a/tests/playwright/_internal/test_e2e_regex_matching.py b/tests/playwright/shiny/_internal/test_e2e_regex_matching.py similarity index 100% rename from tests/playwright/_internal/test_e2e_regex_matching.py rename to tests/playwright/shiny/_internal/test_e2e_regex_matching.py diff --git a/tests/playwright/shiny/shiny-express/accordion/test_accordion.py b/tests/playwright/shiny/shiny-express/accordion/test_accordion.py deleted file mode 100644 index 80edbc6e9..000000000 --- a/tests/playwright/shiny/shiny-express/accordion/test_accordion.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_accordion - -app = create_deploys_fixture("shiny-express-accordion") - - -def test_express_accordion(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_accordion(page) diff --git a/tests/playwright/shiny/shiny-express/dataframe/test_dataframe.py b/tests/playwright/shiny/shiny-express/dataframe/test_dataframe.py deleted file mode 100644 index d48a15c29..000000000 --- a/tests/playwright/shiny/shiny-express/dataframe/test_dataframe.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_dataframe - -app = create_deploys_fixture("shiny-express-dataframe") - - -def test_page_default(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_dataframe(page) diff --git a/tests/playwright/shiny/shiny-express/folium/test_folium.py b/tests/playwright/shiny/shiny-express/folium/test_folium.py deleted file mode 100644 index 0296aaab5..000000000 --- a/tests/playwright/shiny/shiny-express/folium/test_folium.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_folium_render - -app = create_deploys_fixture("shiny-express-folium") - - -def test_folium_map(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_folium_render(page) diff --git a/tests/playwright/shiny/shiny-express/page_default/test_page_default.py b/tests/playwright/shiny/shiny-express/page_default/test_page_default.py deleted file mode 100644 index e815450c0..000000000 --- a/tests/playwright/shiny/shiny-express/page_default/test_page_default.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_page_default - -app = create_deploys_fixture("shiny-express-page-default") - - -def test_page_default(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_page_default(page) diff --git a/tests/playwright/shiny/shiny-express/page_fillable/test_page_fillable.py b/tests/playwright/shiny/shiny-express/page_fillable/test_page_fillable.py deleted file mode 100644 index c1fb518c9..000000000 --- a/tests/playwright/shiny/shiny-express/page_fillable/test_page_fillable.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_page_fillable - -app = create_deploys_fixture("shiny-express-page-fillable") - - -def test_express_page_fillable(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_page_fillable(page) diff --git a/tests/playwright/shiny/shiny-express/page_fluid/test_page_fluid.py b/tests/playwright/shiny/shiny-express/page_fluid/test_page_fluid.py deleted file mode 100644 index 6967af245..000000000 --- a/tests/playwright/shiny/shiny-express/page_fluid/test_page_fluid.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_page_fluid - -app = create_deploys_fixture("shiny-express-page-fluid") - - -def test_express_page_fluid(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_page_fluid(page) diff --git a/tests/playwright/shiny/shiny-express/page_sidebar/test_page_sidebar.py b/tests/playwright/shiny/shiny-express/page_sidebar/test_page_sidebar.py deleted file mode 100644 index 0aedf85a1..000000000 --- a/tests/playwright/shiny/shiny-express/page_sidebar/test_page_sidebar.py +++ /dev/null @@ -1,10 +0,0 @@ -from conftest import ShinyAppProc, create_deploys_fixture -from playwright.sync_api import Page -from utils.express_utils import verify_express_page_sidebar - -app = create_deploys_fixture("shiny-express-page-sidebar") - - -def test_express_page_sidebar(page: Page, app: ShinyAppProc) -> None: - page.goto(app.url) - verify_express_page_sidebar(page) diff --git a/tests/playwright/utils/deploy_utils.py b/tests/playwright/utils/deploy_utils.py index 423c6bc57..71378c9b0 100644 --- a/tests/playwright/utils/deploy_utils.py +++ b/tests/playwright/utils/deploy_utils.py @@ -1,11 +1,20 @@ +from __future__ import annotations + import json import os import subprocess -import typing +from typing import Any, Callable, TypeVar +import pytest import requests +from conftest import ScopeName, local_app_fixture_gen + +LOCAL_LOCATION = "local" -__all__ = ("deploy",) +__all__ = ( + "create_deploys_app_url_fixture", + "skip_if_not_chrome", +) # connect server_url = os.environ.get("DEPLOY_CONNECT_SERVER_URL") @@ -16,13 +25,30 @@ secret = os.environ.get("DEPLOY_SHINYAPPS_SECRET") +deploy_locations = ["connect", "shinyapps"] + +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def skip_if_not_chrome(fn: CallableT) -> CallableT: + # # Keeping commented to allow for easier local debugging + # import platform + # fn = pytest.mark.skipif( + # platform.python_version_tuple()[:2] != ("3", "10"), + # reason="Test requires Python 3.10", + # )(fn) + fn = pytest.mark.only_browser("chromium")(fn) + + return fn + + def exception_swallower( - function: typing.Callable[[str, str], str] -) -> typing.Callable[[str, str], str]: - def wrapper(app_name: str, app_file_path: str) -> str: - runtime_e: typing.Union[Exception, None] = None + function: Callable[[str, str], str] +) -> Callable[[str, str], str]: + def wrapper(app_name: str, app_dir: str) -> str: + runtime_e: Exception | None = None try: - return function(app_name, app_file_path) + return function(app_name, app_dir) except Exception as e: runtime_e = e if isinstance(runtime_e, Exception): @@ -42,14 +68,14 @@ def run_command(cmd: str) -> str: return output.stdout -def deploy_to_connect(app_name: str, app_file_path: str) -> str: +def deploy_to_connect(app_name: str, app_dir: str) -> str: if not api_key: raise RuntimeError("No api key found. Cannot deploy.") # check if connect app is already deployed to avoid duplicates connect_server_lookup_command = f"rsconnect content search --server {server_url} --api-key {api_key} --title-contains {app_name}" app_details = run_command(connect_server_lookup_command) - connect_server_deploy = f"rsconnect deploy shiny {app_file_path} --server {server_url} --api-key {api_key} --title {app_name} --verbose" + connect_server_deploy = f"rsconnect deploy shiny {app_dir} --server {server_url} --api-key {api_key} --title {app_name} --verbose" # only if the app exists do we replace existing app with new version if json.loads(app_details): app_id = json.loads(app_details)[0]["guid"] @@ -79,9 +105,9 @@ def deploy_to_connect(app_name: str, app_file_path: str) -> str: # TODO-future: Supress web browser from opening after deploying - https://github.com/rstudio/rsconnect-python/issues/462 -def deploy_to_shinyapps(app_name: str, app_file_path: str) -> str: +def deploy_to_shinyapps(app_name: str, app_dir: str) -> str: # Deploy to shinyapps.io - shinyapps_deploy = f"rsconnect deploy shiny {app_file_path} --account {name} --token {token} --secret {secret} --title {app_name} --verbose" + shinyapps_deploy = f"rsconnect deploy shiny {app_dir} --account {name} --token {token} --secret {secret} --title {app_name} --verbose" run_command(shinyapps_deploy) return f"https://{name}.shinyapps.io/{app_name}/" @@ -89,23 +115,10 @@ def deploy_to_shinyapps(app_name: str, app_file_path: str) -> str: quiet_deploy_to_shinyapps = exception_swallower(deploy_to_shinyapps) -def deploy(location: str, app_name: str, app_file_path: str) -> str: - deployment_functions = { - "connect": quiet_deploy_to_connect, - "shinyapps": quiet_deploy_to_shinyapps, - } - deployment_function = deployment_functions.get(location) - if deployment_function: - url = deployment_function(app_name, app_file_path) - else: - raise ValueError("Unknown deploy location. Cannot deploy.") - return url - - # Since connect parses python packages, we need to get latest version of shiny on HEAD -def write_requirements_txt(app_file_path: str) -> None: - app_requirements_file_path = os.path.join(app_file_path, "app_requirements.txt") - requirements_file_path = os.path.join(app_file_path, "requirements.txt") +def write_requirements_txt(app_dir: str) -> None: + app_requirements_file_path = os.path.join(app_dir, "app_requirements.txt") + requirements_file_path = os.path.join(app_dir, "requirements.txt") git_cmd = subprocess.run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE) git_hash = git_cmd.stdout.decode("utf-8").strip() with open(app_requirements_file_path) as f: @@ -113,3 +126,66 @@ def write_requirements_txt(app_file_path: str) -> None: with open(requirements_file_path, "w") as f: f.write(f"{requirements}\n") f.write(f"git+https://github.com/posit-dev/py-shiny.git@{git_hash}\n") + + +def deploy_app( + app_file_path: str, + location: str, + app_name: str, +) -> str: + should_deploy_apps = os.environ.get("DEPLOY_APPS", "False") == "true" + + if not should_deploy_apps: + pytest.skip("`DEPLOY_APPS` does not equal `true`") + + run_on_ci = os.environ.get("CI", "False") == "true" + repo = os.environ.get("GITHUB_REPOSITORY", "unknown") + branch_name = os.environ.get("GITHUB_HEAD_REF", "unknown") + + if ( + not run_on_ci + or repo != "posit-dev/py-shiny" + or not (branch_name.startswith("deploy") or branch_name == "main") + ): + pytest.skip("Not on CI or posit-dev/py-shiny repo or deploy* or main branch") + raise RuntimeError() + + app_dir = os.path.dirname(app_file_path) + write_requirements_txt(app_dir) + + deployment_function = { + "connect": quiet_deploy_to_connect, + "shinyapps": quiet_deploy_to_shinyapps, + }[location] + + url = deployment_function(app_name, app_dir) + return url + + +def create_deploys_app_url_fixture( + app_name: str, + scope: ScopeName = "module", +): + @pytest.fixture(scope=scope, params=[*deploy_locations, LOCAL_LOCATION]) + def fix_fn(request: pytest.FixtureRequest): + app_file = os.path.join(os.path.dirname(request.path), "app.py") + deploy_location = request.param + + if deploy_location == LOCAL_LOCATION: + shinyapp_proc_gen = local_app_fixture_gen(app_file) + # Return the `url` + yield next(shinyapp_proc_gen).url + elif deploy_location in deploy_locations: + app_url = deploy_app( + app_file, + deploy_location, + app_name, + ) + yield app_url + + else: + raise ValueError( + "Deploy location not a known location: '", deploy_location, "'" + ) + + return fix_fn diff --git a/tests/playwright/utils/express_utils.py b/tests/playwright/utils/express_utils.py index 1dfa2373e..8fe8c2820 100644 --- a/tests/playwright/utils/express_utils.py +++ b/tests/playwright/utils/express_utils.py @@ -2,28 +2,6 @@ import typing -from controls import ( - Accordion, - Card, - LayoutNavsetTab, - OutputDataFrame, - OutputTextVerbatim, - Sidebar, -) -from playwright.sync_api import Page, expect - -from shiny import ui -from shiny.express import ui as xui - - -def verify_express_accordion(page: Page) -> None: - acc = Accordion(page, "express_accordion") - acc_panel_2 = acc.accordion_panel("Panel 2") - acc_panel_2.expect_open(True) - acc_panel_2.expect_body("n = 50") - acc_panel_2.set(False) - acc_panel_2.expect_open(False) - def compare_annotations( ui_fn: typing.Callable[..., typing.Any], layout_fn: typing.Callable[..., typing.Any] @@ -48,65 +26,3 @@ def compare_annotations( assert layout_val.endswith(ui_val) else: assert ui_a[key] == layout_a[key] - - -def verify_express_page_default(page: Page) -> None: - nav_html = LayoutNavsetTab(page, "express_navset_tab") - nav_html.expect_content("pre 0pre 1pre 2") - nav_html.set("div") - nav_html.expect_content("div 0\ndiv 1\ndiv 2") - nav_html.set("span") - nav_html.expect_content("span 0span 1span 2") - navset_card_tab = LayoutNavsetTab(page, "express_navset_card_tab") - navset_card_tab.expect_content("") - # since it is a custom table we can't use the OutputTable controls - shell_text = page.locator("#shell").inner_text().strip() - assert shell_text == ( - "R1C1R1\nR1C1R2-R1C1R1\nR1C1R2-R1C1R2\nR1C1R2-R1C2\nR1C2" - ), "Locator contents don't match expected text" - - -def verify_express_page_fillable(page: Page) -> None: - card = Card(page, "card") - output_txt = OutputTextVerbatim(page, "txt") - output_txt.expect_value("50") - bounding_box = card.loc.bounding_box() - assert bounding_box is not None - assert bounding_box["height"] > 300 - - -def verify_express_page_fluid(page: Page) -> None: - card = Card(page, "card") - output_txt = OutputTextVerbatim(page, "txt") - output_txt.expect_value("50") - bounding_box = card.loc.bounding_box() - assert bounding_box is not None - assert bounding_box["height"] < 300 - - -def verify_express_page_sidebar(page: Page) -> None: - sidebar = Sidebar(page, "sidebar") - sidebar.expect_text("SidebarTitle Sidebar Content") - output_txt = OutputTextVerbatim(page, "txt") - output_txt.expect_value("50") - compare_annotations(ui.sidebar, xui.sidebar) - - -def verify_express_dataframe(page: Page) -> None: - dataframe = OutputDataFrame(page, "sample_data_frame") - dataframe.expect_n_row(6) - - -def verify_express_folium_render(page: Page) -> None: - expect(page.get_by_text("Static Map")).to_have_count(1) - expect(page.get_by_text("Map inside of render express call")).to_have_count(1) - # map inside the @render.express - expect( - page.frame_locator("iframe").nth(1).get_by_role("link", name="OpenStreetMap") - ).to_have_count(1) - # map outside of the @render.express at the top level - expect( - page.frame_locator("iframe") - .nth(0) - .get_by_role("link", name="U.S. Geological Survey") - ).to_have_count(1)