Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Shared fixtures for iSamples website BDD tests.

Usage:
# Against live site (default):
pytest tests/

# Against local Quarto preview:
ISAMPLES_BASE_URL=http://localhost:5860 pytest tests/

# With visible browser:
pytest tests/ --headed
"""
import os
import pytest
from playwright.sync_api import sync_playwright


SITE_URL = os.environ.get("ISAMPLES_BASE_URL", "https://isamples.org")


@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(
headless="--headed" not in " ".join(os.sys.argv),
)
yield browser
browser.close()


@pytest.fixture
def page(browser):
context = browser.new_context(viewport={"width": 1280, "height": 900})
page = context.new_page()
yield page
context.close()


@pytest.fixture(scope="session")
def site_url():
return SITE_URL
83 changes: 83 additions & 0 deletions tests/test_explorer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
Interactive Explorer tests — verify the search/filter/globe experience.

These tests hit the live Explorer page and wait for DuckDB-WASM to initialize.
They are slower (~30s+) due to remote parquet loading.
"""
import pytest
from conftest import SITE_URL

EXPLORER_URL = f"{SITE_URL}/tutorials/isamples_explorer.html"


@pytest.fixture
def explorer_page(page):
"""Navigate to Explorer and wait for initial load."""
page.goto(EXPLORER_URL, wait_until="domcontentloaded", timeout=60000)
return page


class TestExplorerLoads:
"""Explorer page should load and initialize DuckDB-WASM."""

def test_page_loads(self, explorer_page):
assert "Explorer" in explorer_page.title()

def test_has_search_input(self, explorer_page):
# Observable inputs render after JS executes — wait for them
search = explorer_page.locator("input[type='text']")
search.first.wait_for(state="visible", timeout=15000)
assert search.count() > 0

def test_has_source_filter_section(self, explorer_page):
assert explorer_page.get_by_text("Source", exact=True).count() > 0

def test_has_material_filter_section(self, explorer_page):
assert explorer_page.get_by_text("Material", exact=True).count() > 0

def test_has_sampled_feature_filter(self, explorer_page):
assert explorer_page.get_by_text("Sampled Feature").count() > 0

def test_has_specimen_type_filter(self, explorer_page):
assert explorer_page.get_by_text("Specimen Type").count() > 0

def test_has_max_samples_slider(self, explorer_page):
# Observable range input renders after JS — wait for it
slider = explorer_page.locator("input[type='range']")
slider.first.wait_for(state="attached", timeout=15000)
assert slider.count() > 0

def test_has_view_mode_selector(self, explorer_page):
assert explorer_page.get_by_text("Globe").count() > 0
assert explorer_page.get_by_text("List").count() > 0
assert explorer_page.get_by_text("Table").count() > 0


class TestExplorerFacetCounts:
"""Facet counts should appear from pre-computed summaries."""

def test_source_checkboxes_have_counts(self, explorer_page):
"""Source checkboxes should show sample counts (loaded from 2KB summary)."""
# Wait for facet summaries to load (they're tiny, should be fast)
explorer_page.wait_for_timeout(5000)
# Check that at least one source has a count in parentheses
sesar = explorer_page.get_by_text("SESAR")
assert sesar.count() > 0

def test_four_sources_present(self, explorer_page):
"""All 4 data sources should appear as filter options."""
explorer_page.wait_for_timeout(5000)
for source in ["SESAR", "OPENCONTEXT", "GEOME", "SMITHSONIAN"]:
assert explorer_page.get_by_text(source).count() > 0, f"Missing source: {source}"


class TestExplorerSampleCard:
"""Sample Card section should exist."""

def test_has_sample_card_section(self, explorer_page):
assert explorer_page.get_by_text("Sample Card").count() > 0

def test_sample_card_shows_click_prompt(self, explorer_page):
"""Before clicking a point, card should show instructions."""
explorer_page.wait_for_timeout(3000)
assert explorer_page.get_by_text("Click a point").count() > 0
128 changes: 128 additions & 0 deletions tests/test_key_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
Key page smoke tests — verify all important pages load and have expected content.

These catch broken pages, missing assets, and regressions in page structure.
"""
import pytest
from conftest import SITE_URL


class TestHomepage:
"""Homepage should load with hero, globe animation, and showcase."""

def test_homepage_loads(self, page):
response = page.goto(SITE_URL, wait_until="domcontentloaded")
assert response.status == 200

def test_homepage_has_title(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
assert "iSamples" in page.title()

def test_homepage_has_hero_text(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
assert page.get_by_text("Internet of Samples").count() > 0

def test_homepage_has_globe_animation(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
globe = page.locator("img[src*='isamples_globe']")
assert globe.count() > 0


class TestAboutPage:
"""About page should have all 4 wireframe sections."""

def test_about_loads(self, page):
response = page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
assert response.status == 200

def test_has_objectives(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
assert page.locator("h2:has-text('Objectives')").count() > 0

def test_has_team(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
assert page.locator("h2:has-text('Team')").count() > 0

def test_has_photo_gallery(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
assert page.locator("h2:has-text('Photo Gallery')").count() > 0

def test_has_background_history(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
assert page.locator("h2:has-text('Background')").count() > 0

def test_has_pi_names(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
for name in ["Kerstin Lehnert", "Andrea Thomer", "Neil Davies", "David Vieglais"]:
assert page.get_by_text(name).count() > 0, f"Missing PI: {name}"


class TestHowToUsePage:
"""How to Use page should have quick start and data tables."""

def test_how_to_use_loads(self, page):
response = page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
assert response.status == 200

def test_has_quick_start(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
assert page.get_by_text("Quick Start").count() > 0

def test_has_data_sources_table(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
assert page.get_by_text("SESAR").count() > 0
assert page.get_by_text("OpenContext").count() > 0


class TestArchitecturePage:
"""Architecture overview should have structured sections."""

def test_architecture_loads(self, page):
response = page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
assert response.status == 200

def test_has_core_principles(self, page):
page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
assert page.get_by_text("Core Principles").count() > 0

def test_has_link_to_requirements(self, page):
page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
assert page.locator("a[href*='requirements']").count() > 0

def test_has_link_to_metadata_model(self, page):
page.goto(f"{SITE_URL}/design/index.html", wait_until="domcontentloaded")
assert page.locator("a[href*='metadata']").count() > 0


class TestPublicationsPage:
"""Publications page should have presentations and bibliography."""

def test_publications_loads(self, page):
response = page.goto(f"{SITE_URL}/pubs.html", wait_until="domcontentloaded")
assert response.status == 200

def test_has_presentations_section(self, page):
page.goto(f"{SITE_URL}/pubs.html", wait_until="domcontentloaded")
assert page.get_by_text("Presentations").count() > 0

def test_has_spnhc_talk_link(self, page):
page.goto(f"{SITE_URL}/pubs.html", wait_until="domcontentloaded")
assert page.locator("a[href*='youtu']").count() > 0


class TestDataEndpoint:
"""data.isamples.org should serve parquet files with range requests."""

def test_facet_summaries_accessible(self, page):
# Use Playwright's API request context (not page.goto which triggers download)
response = page.request.head(
"https://data.isamples.org/isamples_202601_facet_summaries.parquet"
)
assert response.status in (200, 206)

def test_wide_parquet_supports_range_requests(self, page):
response = page.request.head(
"https://data.isamples.org/isamples_202601_wide.parquet"
)
assert response.status in (200, 206)
assert "bytes" in response.headers.get("accept-ranges", "")
137 changes: 137 additions & 0 deletions tests/test_navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
Navigation structure tests — verify sidebar and navbar match the April 2026 wireframe.

These tests verify the structural changes from PRs #89 (nav restructure)
and #90 (accordion menus).
"""
import pytest
from conftest import SITE_URL


class TestSidebarSections:
"""Sidebar should show the wireframe's section names."""

def test_sidebar_shows_architecture_and_vocabularies(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Architecture and Vocabularies").count() > 0

def test_sidebar_does_not_show_old_information_architecture(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Information Architecture").count() == 0

def test_sidebar_shows_research_and_resources(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Research & Resources").count() > 0

def test_sidebar_does_not_show_separate_published_research(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Published Research", exact=True).count() == 0

def test_sidebar_does_not_show_separate_resources(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
# "Resources" alone shouldn't appear as a section header
# (it's OK inside "Research & Resources")
sections = sidebar.locator(".sidebar-section .sidebar-section-header")
texts = [s.text_content().strip() for s in sections.all()]
assert "Resources" not in texts


class TestSidebarHowToUse:
"""How to Use section should have 5 sub-items matching wireframe."""

def test_how_to_use_has_overview(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Overview", exact=True).count() > 0

def test_how_to_use_has_deep_dive(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Deep-Dive Analysis").count() > 0

def test_how_to_use_has_globe_viz(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("3D Globe Visualization").count() > 0

def test_how_to_use_has_search_explorer(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Search Explorer").count() > 0

def test_how_to_use_has_narrow_vs_wide(self, page):
page.goto(f"{SITE_URL}/how-to-use.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Technical: Narrow vs Wide").count() > 0


class TestSidebarAbout:
"""About section should have 4 items matching wireframe."""

def test_about_has_objectives(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Objectives", exact=True).count() > 0

def test_about_has_pis_and_contributors(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("PIs and Contributors").count() > 0

def test_about_has_photo_gallery(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Photo Gallery").count() > 0

def test_about_has_background_history(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Background & History").count() > 0


class TestSidebarResearchResources:
"""Research & Resources should have 3 items matching wireframe."""

def test_has_publications_and_conferences(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Publications & Conferences").count() > 0

def test_has_zenodo_community(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Zenodo Community").count() > 0

def test_has_github_repositories(self, page):
page.goto(f"{SITE_URL}/about.html", wait_until="domcontentloaded")
sidebar = page.locator(".sidebar-navigation")
assert sidebar.get_by_text("Github Repositories").count() > 0


class TestNavbar:
"""Top navbar should have the 4 main items."""

def test_navbar_has_home(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
navbar = page.locator(".navbar")
assert navbar.get_by_text("Home", exact=True).count() > 0

def test_navbar_has_interactive_explorer(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
navbar = page.locator(".navbar")
assert navbar.get_by_text("Interactive Explorer").count() > 0

def test_navbar_has_how_to_use(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
navbar = page.locator(".navbar")
assert navbar.get_by_text("How to Use").count() > 0

def test_navbar_has_about(self, page):
page.goto(SITE_URL, wait_until="domcontentloaded")
navbar = page.locator(".navbar")
assert navbar.get_by_text("About", exact=True).count() > 0
Loading
Loading