Skip to content

Commit

Permalink
Migrate MPA e2e test to playwright (streamlit#7859)
Browse files Browse the repository at this point in the history
* Migrate mpa e2e test

* Add copyright header

* Add snapshots

* Remove unneeded snapshots

* Add missing snapshot

* Update test

* Update snapshots

* Revert commit

* Revert snapshots

* Revert snapshot

* Remove wrong snapshot

* Add early execution fixture

* Exclude all pytest ini files

* Clear config option

* Update copyright headers

* Remove snapshot

* Update webkit snapshot

* Update snapshot
  • Loading branch information
lukasmasuch authored Dec 21, 2023
1 parent 3eb3f84 commit 4927708
Show file tree
Hide file tree
Showing 31 changed files with 228 additions and 123 deletions.
99 changes: 0 additions & 99 deletions e2e/specs/multipage_apps.spec.js

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 21 additions & 8 deletions e2e_playwright/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,30 @@
import pytest
import requests
from PIL import Image
from playwright.sync_api import (
Browser,
BrowserContext,
BrowserType,
ElementHandle,
Locator,
Page,
)
from playwright.sync_api import ElementHandle, Locator, Page
from pytest import FixtureRequest


def reorder_early_fixtures(metafunc: pytest.Metafunc):
"""Put fixtures with `pytest.mark.early` first during execution
This allows patch of configurations before the application is initialized
Copied from: https://github.com/pytest-dev/pytest/issues/1216#issuecomment-456109892
"""
for fixturedef in metafunc._arg2fixturedefs.values():
fixturedef = fixturedef[0]
for mark in getattr(fixturedef.func, "pytestmark", []):
if mark.name == "early":
order = metafunc.fixturenames
order.insert(0, order.pop(order.index(fixturedef.argname)))
break


def pytest_generate_tests(metafunc: pytest.Metafunc):
reorder_early_fixtures(metafunc)


class AsyncSubprocess:
"""A context manager. Wraps subprocess. Popen to capture output safely."""

Expand Down
13 changes: 13 additions & 0 deletions e2e_playwright/multipage_apps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
File renamed without changes.
130 changes: 130 additions & 0 deletions e2e_playwright/multipage_apps/mpa_basics_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from playwright.sync_api import Page, expect

from e2e_playwright.conftest import (
ImageCompareFunction,
wait_for_app_loaded,
wait_for_app_run,
)


def test_loads_main_script_on_initial_page_load(app: Page):
"""Test that the main script is loaded on initial page load."""
expect(app.get_by_test_id("stHeading")).to_contain_text("Main Page")


def test_renders_sidebar_nav_correctly(
themed_app: Page, assert_snapshot: ImageCompareFunction
):
"""Test that the sidebar nav is rendered correctly."""
assert_snapshot(themed_app.get_by_test_id("stSidebarNav"), name="mpa-sidebar_nav")


def test_can_switch_between_pages_by_clicking_on_sidebar_links(app: Page):
"""Test that we can switch between pages by clicking on sidebar links."""
app.get_by_test_id("stSidebarNav").locator("a").nth(1).click()
wait_for_app_run(app)
expect(app.get_by_test_id("stHeading")).to_contain_text("Page 2")


def test_supports_navigating_to_page_directly_via_url(page: Page, app_port: int):
"""Test that we can navigate to a page directly via URL."""
page.goto(f"http://localhost:{app_port}/page2")
wait_for_app_loaded(page)

expect(page.get_by_test_id("stHeading")).to_contain_text("Page 2")


def test_can_switch_between_pages_and_edit_widgets(app: Page):
"""Test that we can switch between pages and edit widgets."""
slider = app.locator('.stSlider [role="slider"]')
slider.click()
slider.press("ArrowRight")
wait_for_app_run(app, wait_delay=500)

app.get_by_test_id("stSidebarNav").locator("a").nth(2).click()
wait_for_app_run(app, wait_delay=1000)

expect(app.get_by_test_id("stHeading")).to_contain_text("Page 3")
expect(app.get_by_test_id("stMarkdown")).to_contain_text("x is 0")

slider.click()
slider.press("ArrowRight")
wait_for_app_run(app)

expect(app.get_by_test_id("stMarkdown")).to_contain_text("x is 1")


def test_can_switch_to_the_first_page_with_a_duplicate_name(app: Page):
"""Test that we can switch to the first page with a duplicate name."""
app.get_by_test_id("stSidebarNav").locator("a").nth(3).click()
wait_for_app_run(app)
expect(app.get_by_test_id("stHeading")).to_contain_text("Page 4")


def test_can_switch_to_the_second_page_with_a_duplicate_name(app: Page):
"""Test that we can switch to the second page with a duplicate name."""
app.get_by_test_id("stSidebarNav").locator("a").nth(4).click()
wait_for_app_run(app)
expect(app.get_by_test_id("stHeading")).to_contain_text("Page 5")


def test_runs_the_first_page_with_a_duplicate_name_if_navigating_via_url(
page: Page, app_port: int
):
"""Test that we run the first page with a duplicate name if navigating via URL."""
page.goto(f"http://localhost:{app_port}/page_with_duplicate_name")
wait_for_app_loaded(page)

expect(page.get_by_test_id("stHeading")).to_contain_text("Page 4")


def test_show_not_found_dialog(page: Page, app_port: int):
"""Test that we show a not found dialog if the page doesn't exist."""
page.goto(f"http://localhost:{app_port}/not_a_page")
wait_for_app_loaded(page)

expect(page.locator('[role="dialog"]')).to_contain_text("Page not found")


def test_handles_expand_collapse_of_mpa_nav_correctly(
page: Page, app_port: int, assert_snapshot: ImageCompareFunction
):
"""Test that we handle expand/collapse of MPA nav correctly."""

page.goto(f"http://localhost:{app_port}/page_7")
wait_for_app_loaded(page)

separator = page.get_by_test_id("stSidebarNavSeparator")
svg = separator.locator("svg")

# Expand the nav
svg.click()
assert_snapshot(
page.get_by_test_id("stSidebarNav"), name="mpa-sidebar_nav_expanded"
)

# Collapse the nav
svg.click()
assert_snapshot(
page.get_by_test_id("stSidebarNav"), name="mpa-sidebar_nav_collapsed"
)

# Expand the nav again
svg.click()
assert_snapshot(
page.get_by_test_id("stSidebarNav"), name="mpa-sidebar_nav_expanded"
)
17 changes: 17 additions & 0 deletions e2e_playwright/multipage_apps/mpa_configure_sidebar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import streamlit as st

st.header("App with no sidebar")
34 changes: 34 additions & 0 deletions e2e_playwright/multipage_apps/mpa_configure_sidebar_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

import pytest
from playwright.sync_api import Page, expect


@pytest.fixture(scope="module")
@pytest.mark.early
def configure_hide_sidebar_nav():
"""Configure ui.hideSidebarNav=True."""
# We need to do this in a package scope fixture to ensure that its applied
# before the app server is started.
os.environ["STREAMLIT_UI_HIDE_SIDEBAR_NAV"] = "True"
yield
del os.environ["STREAMLIT_UI_HIDE_SIDEBAR_NAV"]


def test_hides_sidebar_nav(app: Page, configure_hide_sidebar_nav):
"""Test that ui.hideSidebarNav=True hides the sidebar."""
expect(app.get_by_test_id("stSidebar")).not_to_be_attached()
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

st.header("Page 3")
x = st.slider("x")
st.write(f"x is {x}")
st.markdown(f"x is {x}")
8 changes: 8 additions & 0 deletions e2e_playwright/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[pytest]
markers =
early: prioritize the execution of a fixture
filterwarnings =
# PyTest filter syntax cheatsheet -> action:message:category:module:line
ignore::UserWarning:altair.*:
ignore::DeprecationWarning:flatbuffers.*:
ignore::DeprecationWarning:keras_preprocessing.*:
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function Heading(props: HeadingProtoProps): ReactElement {
const [heading, ...rest] = body.split("\n")

return (
<div className="stHeadingContainer">
<div className="stHeadingContainer" data-testid="stHeading">
<div className="stMarkdown" style={{ width }}>
<StyledStreamlitMarkdown
isCaption={Boolean(false)}
Expand Down
4 changes: 3 additions & 1 deletion scripts/check_license_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@
r"|\.(json|prettierrc|nvmrc)$"
# Exclude generated files, because they don't have any degree of creativity.
r"|yarn\.lock$"
# Exclude pytest config files, because they don't have any degree of creativity.
r"|pytest\.ini$"
# Exclude empty files, because they don't have any degree of creativity.
r"|py\.typed$"
# Exclude dev-tools configuration files, because they don't have any
# degree of creativity.
r"|^(\.dockerignore|\.editorconfig|\.gitignore|\.gitmodules)$"
r"|^frontend/(\.dockerignore|\.eslintrc.js|\.prettierignore)$"
r"|^lib/(\.coveragerc|\.dockerignore|MANIFEST\.in|mypy\.ini|pytest\.ini)$"
r"|^lib/(\.coveragerc|\.dockerignore|MANIFEST\.in|mypy\.ini)$"
r"|^lib/(test|dev)-requirements\.txt$"
r"|^lib/min-constraints-gen\.txt"
r"|\.isort\.cfg$"
Expand Down
13 changes: 0 additions & 13 deletions scripts/run_e2e_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,19 +424,6 @@ def run_e2e_tests(
show_output=verbose,
)

elif basename(spec_path) == "multipage_apps.spec.js":
test_name, _ = splitext(basename(spec_path))
test_name, _ = splitext(test_name)
test_path = join(
ctx.tests_dir, "scripts", "multipage_apps", "streamlit_app.py"
)
if os.path.exists(test_path):
run_test(
ctx,
str(spec_path),
["streamlit", "run", test_path],
show_output=verbose,
)
elif basename(spec_path) == "staticfiles_app.spec.js":
test_name, _ = splitext(basename(spec_path))
test_name, _ = splitext(test_name)
Expand Down

0 comments on commit 4927708

Please sign in to comment.