Skip to content

Commit a21712c

Browse files
cpsievertclaude
andcommitted
test(pkg-py): add visualization tests
Add comprehensive test coverage for ggsql visualization features: - test_ggsql.py: Unit tests for ggsql parsing and rendering - test_ggsql_integration.py: Integration tests for end-to-end visualization - test_viz_tools.py: Tests for visualize_dashboard and visualize_query tools - test_visualization_tabs.py: Playwright tests for UI tab interactions - Update test_state.py with visualization state field tests - Update test_tools.py and test_base.py for new tool configurations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4865e70 commit a21712c

8 files changed

Lines changed: 853 additions & 5 deletions

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
"""
2+
Playwright tests for visualization tabs (Filter Plot, Query Plot).
3+
4+
These tests verify that the visualization tabs are present and show
5+
appropriate placeholder messages when no visualization has been created.
6+
7+
Since the visualization tools require real LLM interaction to create charts,
8+
these tests focus on:
9+
1. Tab presence and accessibility
10+
2. Placeholder messages
11+
3. Tab switching functionality
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from typing import TYPE_CHECKING
17+
18+
import pytest
19+
from playwright.sync_api import expect
20+
21+
if TYPE_CHECKING:
22+
from playwright.sync_api import Page
23+
24+
25+
# Shiny Tests
26+
class TestShinyVisualizationTabs:
27+
"""Tests for visualization tabs in Shiny app (01-hello-app.py)."""
28+
29+
@pytest.fixture(autouse=True)
30+
def setup(self, page: Page, app_01_hello: str) -> None:
31+
"""Navigate to the app before each test."""
32+
page.goto(app_01_hello)
33+
page.wait_for_selector("table", timeout=30000)
34+
self.page = page
35+
36+
def test_three_tabs_present(self) -> None:
37+
"""VIZ-SHINY-01: Three tabs are visible (Data, Filter Plot, Query Plot)."""
38+
tabs = self.page.locator('[role="tab"]')
39+
expect(tabs).to_have_count(3)
40+
41+
expect(self.page.get_by_role("tab", name="Data")).to_be_visible()
42+
expect(self.page.get_by_role("tab", name="Filter Plot")).to_be_visible()
43+
expect(self.page.get_by_role("tab", name="Query Plot")).to_be_visible()
44+
45+
def test_filter_plot_tab_clickable(self) -> None:
46+
"""VIZ-SHINY-02: Filter Plot tab can be clicked."""
47+
filter_tab = self.page.locator('text="Filter Plot"')
48+
filter_tab.click()
49+
50+
# Should show placeholder message
51+
expect(self.page.locator("text=No filter visualization")).to_be_visible(
52+
timeout=5000
53+
)
54+
55+
def test_query_plot_tab_clickable(self) -> None:
56+
"""VIZ-SHINY-03: Query Plot tab can be clicked."""
57+
query_tab = self.page.locator('text="Query Plot"')
58+
query_tab.click()
59+
60+
# Should show placeholder message
61+
expect(self.page.locator("text=No query visualization")).to_be_visible(
62+
timeout=5000
63+
)
64+
65+
def test_filter_plot_shows_placeholder(self) -> None:
66+
"""VIZ-SHINY-04: Filter Plot tab shows placeholder when empty."""
67+
filter_tab = self.page.locator('text="Filter Plot"')
68+
filter_tab.click()
69+
70+
placeholder = self.page.locator("text=Use the chat to create one")
71+
expect(placeholder).to_be_visible(timeout=5000)
72+
73+
def test_query_plot_shows_placeholder(self) -> None:
74+
"""VIZ-SHINY-05: Query Plot tab shows placeholder when empty."""
75+
query_tab = self.page.locator('text="Query Plot"')
76+
query_tab.click()
77+
78+
placeholder = self.page.locator("text=Use the chat to create one")
79+
expect(placeholder).to_be_visible(timeout=5000)
80+
81+
def test_can_switch_between_tabs(self) -> None:
82+
"""VIZ-SHINY-06: Can switch between all three tabs."""
83+
# Start on Data tab (default)
84+
expect(self.page.locator("table")).to_be_visible()
85+
86+
# Switch to Filter Plot
87+
self.page.locator('text="Filter Plot"').click()
88+
expect(self.page.locator("text=No filter visualization")).to_be_visible(
89+
timeout=5000
90+
)
91+
92+
# Switch to Query Plot
93+
self.page.locator('text="Query Plot"').click()
94+
expect(self.page.locator("text=No query visualization")).to_be_visible(
95+
timeout=5000
96+
)
97+
98+
# Switch back to Data
99+
self.page.locator('[role="tab"]:has-text("Data")').click()
100+
expect(self.page.locator("table")).to_be_visible()
101+
102+
103+
# Streamlit Tests
104+
class TestStreamlitVisualizationTabs:
105+
"""Tests for visualization tabs in Streamlit app (04-streamlit-app.py)."""
106+
107+
@pytest.fixture(autouse=True)
108+
def setup(self, page: Page, app_04_streamlit: str) -> None:
109+
"""Navigate to the app before each test."""
110+
page.goto(app_04_streamlit)
111+
page.wait_for_selector('[data-testid="stApp"]', timeout=30000)
112+
page.wait_for_selector('[data-testid="stChatMessage"]', timeout=30000)
113+
self.page = page
114+
115+
def test_three_tabs_present(self) -> None:
116+
"""VIZ-STREAMLIT-01: Three tabs are visible."""
117+
tabs = self.page.locator('[data-baseweb="tab"]')
118+
expect(tabs).to_have_count(3)
119+
120+
def test_filter_plot_tab_clickable(self) -> None:
121+
"""VIZ-STREAMLIT-02: Filter Plot tab can be clicked."""
122+
tabs = self.page.locator('[data-baseweb="tab"]')
123+
tabs.nth(1).click() # Filter Plot is second tab
124+
125+
# Should show info message about no visualization
126+
expect(self.page.locator("text=No filter visualization")).to_be_visible(
127+
timeout=5000
128+
)
129+
130+
def test_query_plot_tab_clickable(self) -> None:
131+
"""VIZ-STREAMLIT-03: Query Plot tab can be clicked."""
132+
tabs = self.page.locator('[data-baseweb="tab"]')
133+
tabs.nth(2).click() # Query Plot is third tab
134+
135+
# Should show info message about no visualization
136+
expect(self.page.locator("text=No query visualization")).to_be_visible(
137+
timeout=5000
138+
)
139+
140+
def test_filter_plot_mentions_tool(self) -> None:
141+
"""VIZ-STREAMLIT-04: Filter Plot placeholder mentions the tool."""
142+
tabs = self.page.locator('[data-baseweb="tab"]')
143+
tabs.nth(1).click()
144+
145+
expect(self.page.locator("text=visualize_dashboard")).to_be_visible(
146+
timeout=5000
147+
)
148+
149+
def test_query_plot_mentions_tool(self) -> None:
150+
"""VIZ-STREAMLIT-05: Query Plot placeholder mentions the tool."""
151+
tabs = self.page.locator('[data-baseweb="tab"]')
152+
tabs.nth(2).click()
153+
154+
expect(self.page.locator("text=visualize_query")).to_be_visible(timeout=5000)
155+
156+
157+
# Gradio Tests
158+
class TestGradioVisualizationTabs:
159+
"""Tests for visualization tabs in Gradio app (05-gradio-app.py)."""
160+
161+
@pytest.fixture(autouse=True)
162+
def setup(self, page: Page, app_05_gradio: str) -> None:
163+
"""Navigate to the app before each test."""
164+
page.goto(app_05_gradio)
165+
page.wait_for_selector("gradio-app", timeout=30000)
166+
page.wait_for_selector('[data-testid="bot"]', timeout=30000)
167+
self.page = page
168+
169+
def test_three_tabs_present(self) -> None:
170+
"""VIZ-GRADIO-01: Three tabs are visible."""
171+
tabs = self.page.locator('[role="tab"]')
172+
expect(tabs).to_have_count(3)
173+
174+
def test_filter_plot_tab_clickable(self) -> None:
175+
"""VIZ-GRADIO-02: Filter Plot tab can be clicked."""
176+
filter_tab = self.page.locator('button[role="tab"]:has-text("Filter Plot")')
177+
filter_tab.click()
178+
179+
# Should show placeholder message
180+
expect(self.page.locator("text=No filter visualization")).to_be_visible(
181+
timeout=5000
182+
)
183+
184+
def test_query_plot_tab_clickable(self) -> None:
185+
"""VIZ-GRADIO-03: Query Plot tab can be clicked."""
186+
query_tab = self.page.locator('button[role="tab"]:has-text("Query Plot")')
187+
query_tab.click()
188+
189+
# Should show placeholder message
190+
expect(self.page.locator("text=No query visualization")).to_be_visible(
191+
timeout=5000
192+
)
193+
194+
def test_filter_plot_has_plot_area(self) -> None:
195+
"""VIZ-GRADIO-04: Filter Plot tab has plot and ggsql spec areas."""
196+
filter_tab = self.page.locator('button[role="tab"]:has-text("Filter Plot")')
197+
filter_tab.click()
198+
199+
# Should have Plot label
200+
expect(self.page.locator('text="Plot"')).to_be_visible(timeout=5000)
201+
# Should have ggsql spec label
202+
expect(self.page.locator('text="ggsql spec"')).to_be_visible(timeout=5000)
203+
204+
def test_query_plot_has_plot_area(self) -> None:
205+
"""VIZ-GRADIO-05: Query Plot tab has plot and ggsql query areas."""
206+
query_tab = self.page.locator('button[role="tab"]:has-text("Query Plot")')
207+
query_tab.click()
208+
209+
# Should have Plot label
210+
expect(self.page.locator('text="Plot"')).to_be_visible(timeout=5000)
211+
# Should have ggsql query label
212+
expect(self.page.locator('text="ggsql query"')).to_be_visible(timeout=5000)
213+
214+
215+
# Dash Tests
216+
class TestDashVisualizationTabs:
217+
"""Tests for visualization tabs in Dash app (06-dash-app.py)."""
218+
219+
@pytest.fixture(autouse=True)
220+
def setup(self, page: Page, app_06_dash: str) -> None:
221+
"""Navigate to the app before each test."""
222+
page.goto(app_06_dash)
223+
page.wait_for_selector("#querychat-titanic-chat-history", timeout=30000)
224+
# Wait for greeting
225+
expect(page.locator("#querychat-titanic-chat-history")).to_contain_text(
226+
"Hello", timeout=30000
227+
)
228+
self.page = page
229+
230+
def test_three_tabs_present(self) -> None:
231+
"""VIZ-DASH-01: Three tabs are visible."""
232+
tabs = self.page.locator('[role="tab"]')
233+
expect(tabs).to_have_count(3)
234+
235+
expect(self.page.get_by_role("tab", name="Data")).to_be_visible()
236+
expect(self.page.get_by_role("tab", name="Filter Plot")).to_be_visible()
237+
expect(self.page.get_by_role("tab", name="Query Plot")).to_be_visible()
238+
239+
def test_filter_plot_tab_clickable(self) -> None:
240+
"""VIZ-DASH-02: Filter Plot tab can be clicked."""
241+
filter_tab = self.page.get_by_role("tab", name="Filter Plot")
242+
filter_tab.click()
243+
244+
# Should show placeholder in iframe
245+
filter_plot = self.page.locator("#querychat-titanic-filter-plot")
246+
expect(filter_plot).to_be_visible(timeout=5000)
247+
248+
def test_query_plot_tab_clickable(self) -> None:
249+
"""VIZ-DASH-03: Query Plot tab can be clicked."""
250+
query_tab = self.page.get_by_role("tab", name="Query Plot")
251+
query_tab.click()
252+
253+
# Should show placeholder in iframe
254+
query_plot = self.page.locator("#querychat-titanic-query-plot")
255+
expect(query_plot).to_be_visible(timeout=5000)
256+
257+
def test_data_tab_shows_table(self) -> None:
258+
"""VIZ-DASH-04: Data tab shows the data table."""
259+
# Data tab is default, should show AG Grid. The table wrapper is present
260+
# but the grid may have height: 0 until data loads.
261+
# Check that rows are rendered instead.
262+
data_rows = self.page.locator(".ag-row")
263+
expect(data_rows.first).to_be_visible(timeout=15000)
264+
265+
def test_can_switch_between_tabs(self) -> None:
266+
"""VIZ-DASH-05: Can switch between all three tabs."""
267+
# Start on Data tab (default)
268+
expect(self.page.locator("#querychat-titanic-sql-display")).to_be_visible()
269+
270+
# Switch to Filter Plot
271+
self.page.get_by_role("tab", name="Filter Plot").click()
272+
expect(self.page.locator("#querychat-titanic-filter-plot")).to_be_visible(
273+
timeout=5000
274+
)
275+
276+
# Switch to Query Plot
277+
self.page.get_by_role("tab", name="Query Plot").click()
278+
expect(self.page.locator("#querychat-titanic-query-plot")).to_be_visible(
279+
timeout=5000
280+
)
281+
282+
# Switch back to Data
283+
self.page.get_by_role("tab", name="Data").click()
284+
expect(self.page.locator("#querychat-titanic-sql-display")).to_be_visible(
285+
timeout=5000
286+
)

pkg-py/tests/test_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class TestQueryChatBase:
161161
def test_init_with_dataframe(self, sample_df):
162162
qc = QueryChatBase(sample_df, "test_table")
163163
assert isinstance(qc.data_source, DataFrameSource)
164-
assert qc.tools == ("update", "query")
164+
assert qc.tools == ("update", "query", "visualize_dashboard", "visualize_query")
165165

166166
def test_init_with_custom_greeting(self, sample_df):
167167
qc = QueryChatBase(sample_df, "test_table", greeting="Hello!")

pkg-py/tests/test_client_console.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def test_default_tools_maintain_current_behavior(self, sample_df):
273273
# Without tools parameter, should include both tools (like before)
274274
qc = QueryChat(sample_df, "test_table", greeting="Hello!")
275275

276-
assert qc.tools == ("update", "query")
276+
assert qc.tools == ("update", "query", "visualize_dashboard", "visualize_query")
277277

278278
prompt = qc.system_prompt
279279
assert "Filtering and Sorting Data" in prompt
@@ -292,4 +292,4 @@ def test_existing_initialization_still_works(self, sample_df):
292292

293293
assert qc is not None
294294
assert qc.id == "querychat_test_table"
295-
assert qc.tools == ("update", "query")
295+
assert qc.tools == ("update", "query", "visualize_dashboard", "visualize_query")

0 commit comments

Comments
 (0)