Skip to content

Commit c88db3a

Browse files
committed
Restructure e2e tests
1 parent 43c9e5d commit c88db3a

10 files changed

Lines changed: 2184 additions & 2081 deletions

e2e_playwright/conftest.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
"""Shared Playwright fixtures and pytest hooks for E2E tests."""
1717

1818
from collections.abc import Generator
19+
from pathlib import Path
1920
from typing import Any
2021

2122
import pytest
23+
from e2e_utils import APP_CONFIGS, StreamlitRunner
2224
from playwright.sync_api import Browser, BrowserContext, Page
2325

2426

@@ -61,6 +63,29 @@ def handle_console(msg):
6163
page.close()
6264

6365

66+
@pytest.fixture(scope="module")
67+
def app(request):
68+
"""Start the Streamlit app once per test module."""
69+
module_name = Path(request.module.__file__).name
70+
app_config = APP_CONFIGS.get(module_name, APP_CONFIGS["default"])
71+
72+
with StreamlitRunner(app_config["script"]) as runner:
73+
runner.pivot_keys = app_config["pivot_keys"]
74+
yield runner
75+
76+
77+
@pytest.fixture
78+
def page_at_app(app, page: Page):
79+
"""Navigate to the app and wait for it to be ready."""
80+
page._pivot_keys = app.pivot_keys
81+
page.goto(app.server_url)
82+
page.wait_for_selector("text=Pivot Table E2E Test App", timeout=30000)
83+
page.add_style_tag(
84+
content="header[data-testid='stHeader'] { display: none !important; }"
85+
)
86+
return page
87+
88+
6489
def pytest_configure(config):
6590
config.addinivalue_line(
6691
"markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
@@ -80,7 +105,7 @@ def pytest_collection_modifyitems(config, items):
80105
else:
81106
browser_name = config.getoption("--browser", default=["chromium"])
82107
if isinstance(browser_name, list):
83-
browser_name = browser_name[0]
108+
browser_name = browser_name[0] if browser_name else "chromium"
84109
if browser_name != "chromium":
85110
item.add_marker(
86111
pytest.mark.skip(reason="Clipboard API only works in Chromium")

e2e_playwright/e2e_utils.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
1818
Provides StreamlitRunner, a context manager that starts a Streamlit server
1919
in a subprocess, waits for it to become healthy, and tears it down on exit.
20+
Also provides shared Playwright locator helpers used across E2E test modules.
2021
"""
2122

2223
import contextlib
@@ -29,12 +30,125 @@
2930
import time
3031
import typing
3132
from contextlib import closing
33+
from pathlib import Path
3234
from tempfile import TemporaryFile
3335

3436
import requests
37+
from playwright.sync_api import Locator, Page, expect
3538

3639
LOGGER = logging.getLogger(__file__)
3740

41+
SCRIPT = Path(__file__).parent / "pivot_table.py"
42+
TOOLBAR_SCRIPT = Path(__file__).parent / "pivot_table_toolbar_app.py"
43+
INTERACTIONS_SCRIPT = Path(__file__).parent / "pivot_table_interactions_app.py"
44+
DATA_SCRIPT = Path(__file__).parent / "pivot_table_data_app.py"
45+
46+
PIVOT_KEYS = [
47+
"test_pivot",
48+
"test_pivot_subtotals",
49+
"test_pivot_locked",
50+
"test_pivot_locked_groups",
51+
"test_pivot_cond_fmt",
52+
"test_pivot_readonly",
53+
"test_pivot_number_fmt",
54+
"test_pivot_drilldown",
55+
"test_pivot_empty",
56+
"test_pivot_single_row",
57+
"test_pivot_no_cols",
58+
"test_pivot_count_distinct",
59+
"test_pivot_median",
60+
"test_pivot_auto",
61+
"test_pivot_threshold",
62+
"test_pivot_col_groups",
63+
"test_pivot_alignment",
64+
"test_pivot_tall",
65+
"test_pivot_null_separate",
66+
"test_pivot_null_zero",
67+
"test_pivot_dim_toggle",
68+
"test_pivot_no_drilldown",
69+
"test_pivot_per_dim_subtotals",
70+
"test_pivot_per_measure_row_totals",
71+
"test_pivot_per_measure_col_totals",
72+
"test_pivot_sparse_drilldown",
73+
"test_pivot_synthetic",
74+
"test_pivot_scalar_roundtrip",
75+
]
76+
77+
TOOLBAR_PIVOT_KEYS = [
78+
"test_pivot",
79+
"test_pivot_subtotals",
80+
"test_pivot_cond_fmt",
81+
"test_pivot_scalar_roundtrip",
82+
]
83+
84+
INTERACTIONS_PIVOT_KEYS = [
85+
"test_pivot",
86+
"test_pivot_subtotals",
87+
"test_pivot_cond_fmt",
88+
"test_pivot_locked",
89+
"test_pivot_locked_groups",
90+
"test_pivot_readonly",
91+
"test_pivot_drilldown",
92+
"test_pivot_no_drilldown",
93+
"test_pivot_dim_toggle",
94+
"test_pivot_per_dim_subtotals",
95+
"test_pivot_per_measure_row_totals",
96+
"test_pivot_per_measure_col_totals",
97+
"test_pivot_col_groups",
98+
]
99+
100+
DATA_PIVOT_KEYS = [
101+
"test_pivot",
102+
"test_pivot_cond_fmt",
103+
"test_pivot_number_fmt",
104+
"test_pivot_empty",
105+
"test_pivot_single_row",
106+
"test_pivot_no_cols",
107+
"test_pivot_count_distinct",
108+
"test_pivot_median",
109+
"test_pivot_auto",
110+
"test_pivot_threshold",
111+
"test_pivot_tall",
112+
"test_pivot_alignment",
113+
"test_pivot_null_separate",
114+
"test_pivot_null_zero",
115+
"test_pivot_sparse_drilldown",
116+
"test_pivot_synthetic",
117+
]
118+
119+
APP_CONFIGS = {
120+
"default": {"script": SCRIPT, "pivot_keys": PIVOT_KEYS},
121+
"pivot_table_test.py": {"script": TOOLBAR_SCRIPT, "pivot_keys": TOOLBAR_PIVOT_KEYS},
122+
"pivot_table_interactions_test.py": {
123+
"script": INTERACTIONS_SCRIPT,
124+
"pivot_keys": INTERACTIONS_PIVOT_KEYS,
125+
},
126+
"pivot_table_data_test.py": {"script": DATA_SCRIPT, "pivot_keys": DATA_PIVOT_KEYS},
127+
}
128+
129+
130+
def get_pivot(page: Page, key: str) -> Locator:
131+
"""Return a Locator scoped to the pivot-container for *key*."""
132+
pivot_keys = getattr(page, "_pivot_keys", PIVOT_KEYS)
133+
idx = pivot_keys.index(key)
134+
container = page.get_by_test_id("pivot-container").nth(idx)
135+
container.evaluate("el => el.scrollIntoView({ block: 'center' })")
136+
return container
137+
138+
139+
def open_settings_popover(page: Page, container: Locator) -> Locator:
140+
"""Open the gear settings popover in the toolbar."""
141+
panel = page.get_by_test_id("toolbar-settings-panel")
142+
if panel.count():
143+
expect(panel).to_be_visible(timeout=5000)
144+
return panel
145+
146+
button = container.get_by_test_id("toolbar-settings")
147+
button.scroll_into_view_if_needed()
148+
button.evaluate("el => el.click()")
149+
expect(panel).to_be_visible(timeout=5000)
150+
return panel
151+
38152

39153
def _find_free_port() -> int:
40154
"""Find and return a free port on the local machine."""

0 commit comments

Comments
 (0)