Skip to content

Commit ec95b7c

Browse files
rdhyeeclaude
andauthored
Add BDD-style Playwright test suite for website verification (#91)
62 tests across 4 test files: - test_navigation.py: sidebar sections, navbar, wireframe alignment (21 tests) - test_requirements_page.py: accordion behavior, 18 callouts (7 tests) - test_key_pages.py: homepage, about, how-to-use, architecture, pubs, data endpoint (22 tests) - test_explorer.py: search, filters, facet counts, sample card, view modes (12 tests) Run with: pytest tests/ -p no:base-url Against local: ISAMPLES_BASE_URL=http://localhost:5860 pytest tests/ Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fabe63d commit ec95b7c

5 files changed

Lines changed: 443 additions & 0 deletions

File tree

tests/conftest.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Shared fixtures for iSamples website BDD tests.
3+
4+
Usage:
5+
# Against live site (default):
6+
pytest tests/
7+
8+
# Against local Quarto preview:
9+
ISAMPLES_BASE_URL=http://localhost:5860 pytest tests/
10+
11+
# With visible browser:
12+
pytest tests/ --headed
13+
"""
14+
import os
15+
import pytest
16+
from playwright.sync_api import sync_playwright
17+
18+
19+
SITE_URL = os.environ.get("ISAMPLES_BASE_URL", "https://isamples.org")
20+
21+
22+
@pytest.fixture(scope="session")
23+
def browser():
24+
with sync_playwright() as p:
25+
browser = p.chromium.launch(
26+
headless="--headed" not in " ".join(os.sys.argv),
27+
)
28+
yield browser
29+
browser.close()
30+
31+
32+
@pytest.fixture
33+
def page(browser):
34+
context = browser.new_context(viewport={"width": 1280, "height": 900})
35+
page = context.new_page()
36+
yield page
37+
context.close()
38+
39+
40+
@pytest.fixture(scope="session")
41+
def site_url():
42+
return SITE_URL

tests/test_explorer.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
Interactive Explorer tests — verify the search/filter/globe experience.
3+
4+
These tests hit the live Explorer page and wait for DuckDB-WASM to initialize.
5+
They are slower (~30s+) due to remote parquet loading.
6+
"""
7+
import pytest
8+
from conftest import SITE_URL
9+
10+
EXPLORER_URL = f"{SITE_URL}/tutorials/isamples_explorer.html"
11+
12+
13+
@pytest.fixture
14+
def explorer_page(page):
15+
"""Navigate to Explorer and wait for initial load."""
16+
page.goto(EXPLORER_URL, wait_until="domcontentloaded", timeout=60000)
17+
return page
18+
19+
20+
class TestExplorerLoads:
21+
"""Explorer page should load and initialize DuckDB-WASM."""
22+
23+
def test_page_loads(self, explorer_page):
24+
assert "Explorer" in explorer_page.title()
25+
26+
def test_has_search_input(self, explorer_page):
27+
# Observable inputs render after JS executes — wait for them
28+
search = explorer_page.locator("input[type='text']")
29+
search.first.wait_for(state="visible", timeout=15000)
30+
assert search.count() > 0
31+
32+
def test_has_source_filter_section(self, explorer_page):
33+
assert explorer_page.get_by_text("Source", exact=True).count() > 0
34+
35+
def test_has_material_filter_section(self, explorer_page):
36+
assert explorer_page.get_by_text("Material", exact=True).count() > 0
37+
38+
def test_has_sampled_feature_filter(self, explorer_page):
39+
assert explorer_page.get_by_text("Sampled Feature").count() > 0
40+
41+
def test_has_specimen_type_filter(self, explorer_page):
42+
assert explorer_page.get_by_text("Specimen Type").count() > 0
43+
44+
def test_has_max_samples_slider(self, explorer_page):
45+
# Observable range input renders after JS — wait for it
46+
slider = explorer_page.locator("input[type='range']")
47+
slider.first.wait_for(state="attached", timeout=15000)
48+
assert slider.count() > 0
49+
50+
def test_has_view_mode_selector(self, explorer_page):
51+
assert explorer_page.get_by_text("Globe").count() > 0
52+
assert explorer_page.get_by_text("List").count() > 0
53+
assert explorer_page.get_by_text("Table").count() > 0
54+
55+
56+
class TestExplorerFacetCounts:
57+
"""Facet counts should appear from pre-computed summaries."""
58+
59+
def test_source_checkboxes_have_counts(self, explorer_page):
60+
"""Source checkboxes should show sample counts (loaded from 2KB summary)."""
61+
# Wait for facet summaries to load (they're tiny, should be fast)
62+
explorer_page.wait_for_timeout(5000)
63+
# Check that at least one source has a count in parentheses
64+
sesar = explorer_page.get_by_text("SESAR")
65+
assert sesar.count() > 0
66+
67+
def test_four_sources_present(self, explorer_page):
68+
"""All 4 data sources should appear as filter options."""
69+
explorer_page.wait_for_timeout(5000)
70+
for source in ["SESAR", "OPENCONTEXT", "GEOME", "SMITHSONIAN"]:
71+
assert explorer_page.get_by_text(source).count() > 0, f"Missing source: {source}"
72+
73+
74+
class TestExplorerSampleCard:
75+
"""Sample Card section should exist."""
76+
77+
def test_has_sample_card_section(self, explorer_page):
78+
assert explorer_page.get_by_text("Sample Card").count() > 0
79+
80+
def test_sample_card_shows_click_prompt(self, explorer_page):
81+
"""Before clicking a point, card should show instructions."""
82+
explorer_page.wait_for_timeout(3000)
83+
assert explorer_page.get_by_text("Click a point").count() > 0

tests/test_key_pages.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Key page smoke tests — verify all important pages load and have expected content.
3+
4+
These catch broken pages, missing assets, and regressions in page structure.
5+
"""
6+
import pytest
7+
from conftest import SITE_URL
8+
9+
10+
class TestHomepage:
11+
"""Homepage should load with hero, globe animation, and showcase."""
12+
13+
def test_homepage_loads(self, page):
14+
response = page.goto(SITE_URL, wait_until="domcontentloaded")
15+
assert response.status == 200
16+
17+
def test_homepage_has_title(self, page):
18+
page.goto(SITE_URL, wait_until="domcontentloaded")
19+
assert "iSamples" in page.title()
20+
21+
def test_homepage_has_hero_text(self, page):
22+
page.goto(SITE_URL, wait_until="domcontentloaded")
23+
assert page.get_by_text("Internet of Samples").count() > 0
24+
25+
def test_homepage_has_globe_animation(self, page):
26+
page.goto(SITE_URL, wait_until="domcontentloaded")
27+
globe = page.locator("img[src*='isamples_globe']")
28+
assert globe.count() > 0
29+
30+
31+
class TestAboutPage:
32+
"""About page should have all 4 wireframe sections."""
33+
34+
def test_about_loads(self, page):
35+
response = page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
36+
assert response.status == 200
37+
38+
def test_has_objectives(self, page):
39+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
40+
assert page.locator("h2:has-text('Objectives')").count() > 0
41+
42+
def test_has_team(self, page):
43+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
44+
assert page.locator("h2:has-text('Team')").count() > 0
45+
46+
def test_has_photo_gallery(self, page):
47+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
48+
assert page.locator("h2:has-text('Photo Gallery')").count() > 0
49+
50+
def test_has_background_history(self, page):
51+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
52+
assert page.locator("h2:has-text('Background')").count() > 0
53+
54+
def test_has_pi_names(self, page):
55+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
56+
for name in ["Kerstin Lehnert", "Andrea Thomer", "Neil Davies", "David Vieglais"]:
57+
assert page.get_by_text(name).count() > 0, f"Missing PI: {name}"
58+
59+
60+
class TestHowToUsePage:
61+
"""How to Use page should have quick start and data tables."""
62+
63+
def test_how_to_use_loads(self, page):
64+
response = page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
65+
assert response.status == 200
66+
67+
def test_has_quick_start(self, page):
68+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
69+
assert page.get_by_text("Quick Start").count() > 0
70+
71+
def test_has_data_sources_table(self, page):
72+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
73+
assert page.get_by_text("SESAR").count() > 0
74+
assert page.get_by_text("OpenContext").count() > 0
75+
76+
77+
class TestArchitecturePage:
78+
"""Architecture overview should have structured sections."""
79+
80+
def test_architecture_loads(self, page):
81+
response = page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
82+
assert response.status == 200
83+
84+
def test_has_core_principles(self, page):
85+
page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
86+
assert page.get_by_text("Core Principles").count() > 0
87+
88+
def test_has_link_to_requirements(self, page):
89+
page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
90+
assert page.locator("a[href*='requirements']").count() > 0
91+
92+
def test_has_link_to_metadata_model(self, page):
93+
page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
94+
assert page.locator("a[href*='metadata']").count() > 0
95+
96+
97+
class TestPublicationsPage:
98+
"""Publications page should have presentations and bibliography."""
99+
100+
def test_publications_loads(self, page):
101+
response = page.goto(f"{SITE_URL}/pubs.html", wait_until="domcontentloaded")
102+
assert response.status == 200
103+
104+
def test_has_presentations_section(self, page):
105+
page.goto(f"{SITE_URL}/pubs.html", wait_until="domcontentloaded")
106+
assert page.get_by_text("Presentations").count() > 0
107+
108+
def test_has_spnhc_talk_link(self, page):
109+
page.goto(f"{SITE_URL}/pubs.html", wait_until="domcontentloaded")
110+
assert page.locator("a[href*='youtu']").count() > 0
111+
112+
113+
class TestDataEndpoint:
114+
"""data.isamples.org should serve parquet files with range requests."""
115+
116+
def test_facet_summaries_accessible(self, page):
117+
# Use Playwright's API request context (not page.goto which triggers download)
118+
response = page.request.head(
119+
"https://data.isamples.org/isamples_202601_facet_summaries.parquet"
120+
)
121+
assert response.status in (200, 206)
122+
123+
def test_wide_parquet_supports_range_requests(self, page):
124+
response = page.request.head(
125+
"https://data.isamples.org/isamples_202601_wide.parquet"
126+
)
127+
assert response.status in (200, 206)
128+
assert "bytes" in response.headers.get("accept-ranges", "")

tests/test_navigation.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Navigation structure tests — verify sidebar and navbar match the April 2026 wireframe.
3+
4+
These tests verify the structural changes from PRs #89 (nav restructure)
5+
and #90 (accordion menus).
6+
"""
7+
import pytest
8+
from conftest import SITE_URL
9+
10+
11+
class TestSidebarSections:
12+
"""Sidebar should show the wireframe's section names."""
13+
14+
def test_sidebar_shows_architecture_and_vocabularies(self, page):
15+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
16+
sidebar = page.locator(".sidebar-navigation")
17+
assert sidebar.get_by_text("Architecture and Vocabularies").count() > 0
18+
19+
def test_sidebar_does_not_show_old_information_architecture(self, page):
20+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
21+
sidebar = page.locator(".sidebar-navigation")
22+
assert sidebar.get_by_text("Information Architecture").count() == 0
23+
24+
def test_sidebar_shows_research_and_resources(self, page):
25+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
26+
sidebar = page.locator(".sidebar-navigation")
27+
assert sidebar.get_by_text("Research & Resources").count() > 0
28+
29+
def test_sidebar_does_not_show_separate_published_research(self, page):
30+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
31+
sidebar = page.locator(".sidebar-navigation")
32+
assert sidebar.get_by_text("Published Research", exact=True).count() == 0
33+
34+
def test_sidebar_does_not_show_separate_resources(self, page):
35+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
36+
sidebar = page.locator(".sidebar-navigation")
37+
# "Resources" alone shouldn't appear as a section header
38+
# (it's OK inside "Research & Resources")
39+
sections = sidebar.locator(".sidebar-section .sidebar-section-header")
40+
texts = [s.text_content().strip() for s in sections.all()]
41+
assert "Resources" not in texts
42+
43+
44+
class TestSidebarHowToUse:
45+
"""How to Use section should have 5 sub-items matching wireframe."""
46+
47+
def test_how_to_use_has_overview(self, page):
48+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
49+
sidebar = page.locator(".sidebar-navigation")
50+
assert sidebar.get_by_text("Overview", exact=True).count() > 0
51+
52+
def test_how_to_use_has_deep_dive(self, page):
53+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
54+
sidebar = page.locator(".sidebar-navigation")
55+
assert sidebar.get_by_text("Deep-Dive Analysis").count() > 0
56+
57+
def test_how_to_use_has_globe_viz(self, page):
58+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
59+
sidebar = page.locator(".sidebar-navigation")
60+
assert sidebar.get_by_text("3D Globe Visualization").count() > 0
61+
62+
def test_how_to_use_has_search_explorer(self, page):
63+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
64+
sidebar = page.locator(".sidebar-navigation")
65+
assert sidebar.get_by_text("Search Explorer").count() > 0
66+
67+
def test_how_to_use_has_narrow_vs_wide(self, page):
68+
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
69+
sidebar = page.locator(".sidebar-navigation")
70+
assert sidebar.get_by_text("Technical: Narrow vs Wide").count() > 0
71+
72+
73+
class TestSidebarAbout:
74+
"""About section should have 4 items matching wireframe."""
75+
76+
def test_about_has_objectives(self, page):
77+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
78+
sidebar = page.locator(".sidebar-navigation")
79+
assert sidebar.get_by_text("Objectives", exact=True).count() > 0
80+
81+
def test_about_has_pis_and_contributors(self, page):
82+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
83+
sidebar = page.locator(".sidebar-navigation")
84+
assert sidebar.get_by_text("PIs and Contributors").count() > 0
85+
86+
def test_about_has_photo_gallery(self, page):
87+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
88+
sidebar = page.locator(".sidebar-navigation")
89+
assert sidebar.get_by_text("Photo Gallery").count() > 0
90+
91+
def test_about_has_background_history(self, page):
92+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
93+
sidebar = page.locator(".sidebar-navigation")
94+
assert sidebar.get_by_text("Background & History").count() > 0
95+
96+
97+
class TestSidebarResearchResources:
98+
"""Research & Resources should have 3 items matching wireframe."""
99+
100+
def test_has_publications_and_conferences(self, page):
101+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
102+
sidebar = page.locator(".sidebar-navigation")
103+
assert sidebar.get_by_text("Publications & Conferences").count() > 0
104+
105+
def test_has_zenodo_community(self, page):
106+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
107+
sidebar = page.locator(".sidebar-navigation")
108+
assert sidebar.get_by_text("Zenodo Community").count() > 0
109+
110+
def test_has_github_repositories(self, page):
111+
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
112+
sidebar = page.locator(".sidebar-navigation")
113+
assert sidebar.get_by_text("Github Repositories").count() > 0
114+
115+
116+
class TestNavbar:
117+
"""Top navbar should have the 4 main items."""
118+
119+
def test_navbar_has_home(self, page):
120+
page.goto(SITE_URL, wait_until="domcontentloaded")
121+
navbar = page.locator(".navbar")
122+
assert navbar.get_by_text("Home", exact=True).count() > 0
123+
124+
def test_navbar_has_interactive_explorer(self, page):
125+
page.goto(SITE_URL, wait_until="domcontentloaded")
126+
navbar = page.locator(".navbar")
127+
assert navbar.get_by_text("Interactive Explorer").count() > 0
128+
129+
def test_navbar_has_how_to_use(self, page):
130+
page.goto(SITE_URL, wait_until="domcontentloaded")
131+
navbar = page.locator(".navbar")
132+
assert navbar.get_by_text("How to Use").count() > 0
133+
134+
def test_navbar_has_about(self, page):
135+
page.goto(SITE_URL, wait_until="domcontentloaded")
136+
navbar = page.locator(".navbar")
137+
assert navbar.get_by_text("About", exact=True).count() > 0

0 commit comments

Comments
 (0)