mobile responsiveness

This commit is contained in:
whilb 2025-09-01 18:37:33 -07:00
parent b93349aa4d
commit ad664c32ea
4 changed files with 494 additions and 3 deletions

View file

@ -65,16 +65,135 @@ select:disabled{ opacity:.55; cursor:not-allowed; }
html,body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,Segoe UI,Roboto,Ubuntu,Cantarell,sans-serif}
.wrap{max-width:var(--max);margin:0 auto;padding:16px}
.bar{position:sticky;top:0;background:linear-gradient(180deg,rgba(0,0,0,.06),rgba(0,0,0,0));backdrop-filter:blur(8px);border-bottom:1px solid var(--border);z-index:10}
.bar__inner{display:flex;align-items:center;gap:12px;justify-content:space-between}
.bar{position:sticky;top:0;background:linear-gradient(180deg,rgba(0,0,0,.06),rgba(0,0,0,0));backdrop-filter:blur(8px);border-bottom:1px solid var(--border);z-index:10;min-height:70px}
.bar__inner{display:flex;align-items:center;gap:12px;justify-content:space-between;min-height:70px}
.brand{font-weight:700}
.btn{background:transparent;border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:999px;cursor:pointer}
/* Mobile navigation toggle button */
.nav-toggle {
display: none;
background: transparent;
border: 1px solid var(--border);
color: var(--text);
padding: 8px;
border-radius: 8px;
cursor: pointer;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.nav-toggle svg {
width: 20px;
height: 20px;
fill: currentColor;
}
/* ---- Layout ---- */
.layout{display:grid;grid-template-columns:240px 1fr;gap:16px}
@media (max-width: 820px){
.layout{grid-template-columns:1fr}
/* Show mobile nav toggle */
.nav-toggle {
display: flex !important;
}
/* Hide desktop navigation by default on mobile */
.sidenav {
display: none;
position: fixed;
top: 70px;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
background: var(--card);
border-radius: 0;
border: none;
box-shadow: var(--shadow);
overflow-y: auto;
}
/* Show navigation when active */
.sidenav.mobile-active {
display: block;
}
/* Add mobile overlay */
.sidenav::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
/* Adjust main content spacing for mobile */
.content {
margin-top: 16px;
}
/* Improve mobile spacing */
.wrap {
padding: 12px;
}
/* Better mobile grid */
.content {
grid-template-columns: 1fr;
gap: 12px;
}
/* Mobile-friendly cards */
.card {
padding: 12px;
}
/* Mobile-friendly inputs */
input, select, textarea {
padding: 12px;
font-size: 16px; /* Prevents zoom on iOS */
}
/* Mobile-friendly results */
.result {
margin-top: 16px;
padding: 12px;
overflow-x: auto;
}
/* Mobile-friendly tables */
table {
font-size: 14px;
}
/* Mobile-friendly calculator inputs */
.calculator-container {
padding: 16px 0;
}
/* Ensure proper spacing from navigation */
.layout {
padding-top: 16px;
}
/* Mobile-friendly footer */
.footer-content {
flex-direction: column;
gap: 12px;
text-align: center;
}
.source-link {
margin-left: 0;
}
}
/* ---- Vertical nav ---- */

View file

@ -14,7 +14,14 @@
<header class="bar">
<div class="wrap bar__inner">
<div class="brand">calculator.127local.net</div>
<button id="themeToggle" class="btn" aria-label="Toggle color scheme">Auto</button>
<div style="display: flex; align-items: center; gap: 12px;">
<button id="navToggle" class="nav-toggle" aria-label="Toggle navigation">
<svg viewBox="0 0 24 24">
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
</button>
<button id="themeToggle" class="btn" aria-label="Toggle color scheme">Auto</button>
</div>
</div>
</header>

View file

@ -12,8 +12,29 @@ const CALCS = [
const navEl = document.getElementById('nav');
const viewEl = document.getElementById('view');
const themeBtn= document.getElementById('themeToggle');
const navToggleBtn = document.getElementById('navToggle');
initTheme(themeBtn);
// Mobile navigation toggle
navToggleBtn.addEventListener('click', () => {
navEl.classList.toggle('mobile-active');
});
// Close mobile nav when clicking outside
document.addEventListener('click', (e) => {
if (!navEl.contains(e.target) && !navToggleBtn.contains(e.target)) {
navEl.classList.remove('mobile-active');
}
});
// Close mobile nav when clicking on a nav link
navEl.addEventListener('click', (e) => {
const a = e.target.closest('a[data-calc]');
if (a) {
navEl.classList.remove('mobile-active');
}
});
const moduleCache = new Map();
const viewCache = new Map();

344
tests/test_mobile.py Normal file
View file

@ -0,0 +1,344 @@
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
class TestMobileResponsiveness:
"""Test mobile responsiveness and navigation functionality"""
def test_mobile_nav_toggle_button_exists(self, calculator_page):
"""Test that mobile navigation toggle button is present"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
assert nav_toggle.is_displayed()
# Check it has the correct class
assert "nav-toggle" in nav_toggle.get_attribute("class")
# Check it has the hamburger icon
svg = nav_toggle.find_element(By.TAG_NAME, "svg")
assert svg.is_displayed()
def test_mobile_nav_toggle_functionality(self, calculator_page):
"""Test that mobile navigation toggle works correctly"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
sidenav = calculator_page.find_element(By.ID, "nav")
# Debug: check if element is actually clickable
print(f"Nav toggle displayed: {nav_toggle.is_displayed()}")
print(f"Nav toggle enabled: {nav_toggle.is_enabled()}")
print(f"Nav toggle location: {nav_toggle.location}")
print(f"Nav toggle size: {nav_toggle.size}")
# Initially, sidenav should not have mobile-active class
assert "mobile-active" not in sidenav.get_attribute("class")
# Wait for element to be clickable
WebDriverWait(calculator_page, 10).until(
EC.element_to_be_clickable((By.ID, "navToggle"))
)
# Click the toggle button using JavaScript if regular click fails
try:
nav_toggle.click()
except Exception as e:
print(f"Regular click failed: {e}")
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Wait for the mobile-active class to be added
WebDriverWait(calculator_page, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
# Click again to close
try:
nav_toggle.click()
except Exception as e:
print(f"Regular click failed on close: {e}")
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Wait for the mobile-active class to be removed
WebDriverWait(calculator_page, 5).until_not(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
def test_mobile_nav_closes_on_outside_click(self, calculator_page):
"""Test that mobile navigation closes when clicking outside"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
sidenav = calculator_page.find_element(By.ID, "nav")
# Open mobile nav using JavaScript
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Wait for nav to open
WebDriverWait(calculator_page, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
# Click on the body element (outside nav) using JavaScript
calculator_page.execute_script("document.body.click();")
# Wait for nav to close
WebDriverWait(calculator_page, 5).until_not(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
def test_mobile_nav_closes_on_nav_link_click(self, calculator_page):
"""Test that mobile navigation closes when clicking a navigation link"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
sidenav = calculator_page.find_element(By.ID, "nav")
# Open mobile nav using JavaScript
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Wait for nav to open
WebDriverWait(calculator_page, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
# Click on a navigation link
nav_link = sidenav.find_element(By.CSS_SELECTOR, "a[data-calc='raid']")
nav_link.click()
# Wait for nav to close
WebDriverWait(calculator_page, 5).until_not(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
def test_mobile_nav_sticky_positioning(self, calculator_page):
"""Test that navigation bar stays at top and content doesn't scroll under it"""
# Get the navigation bar
nav_bar = calculator_page.find_element(By.CLASS_NAME, "bar")
# Check that it has sticky positioning
position = nav_bar.value_of_css_property("position")
assert position == "sticky"
# Check that it has a high z-index
z_index = nav_bar.value_of_css_property("z-index")
assert int(z_index) >= 10
# Check that it has a minimum height
min_height = nav_bar.value_of_css_property("min-height")
assert min_height == "70px"
def test_mobile_responsive_layout(self, calculator_page):
"""Test that layout changes appropriately on mobile"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
# Get the layout container
layout = calculator_page.find_element(By.CLASS_NAME, "layout")
# Check that it has proper grid layout
display = layout.value_of_css_property("display")
assert display == "grid"
# Check that it has responsive grid template
grid_template = layout.value_of_css_property("grid-template-columns")
# Should be responsive - on mobile it will be 1fr, on desktop 240px 1fr
# The actual value might be computed differently, so just check it's a valid grid value
assert "px" in grid_template or "fr" in grid_template
def test_mobile_friendly_inputs(self, calculator_page):
"""Test that inputs are mobile-friendly"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
# Navigate to a calculator with inputs
calculator_page.get("http://localhost:8008/subnet")
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']"))
)
# Check input styling
ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']")
# Check padding (should be 12px on mobile)
padding = ip_input.value_of_css_property("padding")
assert "12px" in padding
# Check font size (should be 16px to prevent zoom on iOS)
font_size = ip_input.value_of_css_property("font-size")
assert "16px" in font_size
def test_mobile_table_overflow(self, calculator_page):
"""Test that tables have horizontal scroll on mobile"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
# Navigate to subnet calculator which has tables
calculator_page.get("http://localhost:8008/subnet")
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']"))
)
# Enter an IP address to generate the table
ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']")
ip_input.clear()
ip_input.send_keys("192.168.1.1")
# Wait for table to appear
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "table"))
)
# Check that the result container has overflow handling
result_container = calculator_page.find_element(By.CLASS_NAME, "result")
overflow_x = result_container.value_of_css_property("overflow-x")
# Should have auto or scroll overflow on mobile
assert overflow_x in ["auto", "scroll"]
def test_mobile_footer_layout(self, calculator_page):
"""Test that footer is mobile-friendly"""
footer_content = calculator_page.find_element(By.CLASS_NAME, "footer-content")
# Check that footer content has proper flexbox layout
display = footer_content.value_of_css_property("display")
assert display == "flex"
# Check that source link is properly positioned
source_link = calculator_page.find_element(By.CLASS_NAME, "source-link")
assert source_link.is_displayed()
assert "https://code.disobey.net/whilb/calculator.127local.net" in source_link.get_attribute("href")
def test_mobile_nav_theme_toggle_buttons(self, calculator_page):
"""Test that both nav toggle and theme toggle buttons are accessible"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
theme_toggle = calculator_page.find_element(By.ID, "themeToggle")
# Both buttons should be visible
assert nav_toggle.is_displayed()
assert theme_toggle.is_displayed()
# Both should be clickable
assert nav_toggle.is_enabled()
assert theme_toggle.is_enabled()
# Check button styling
for button in [nav_toggle, theme_toggle]:
cursor = button.value_of_css_property("cursor")
assert cursor == "pointer"
def test_mobile_nav_accessibility(self, calculator_page):
"""Test mobile navigation accessibility features"""
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
sidenav = calculator_page.find_element(By.ID, "nav")
# Check aria-label on toggle button
aria_label = nav_toggle.get_attribute("aria-label")
assert aria_label == "Toggle navigation"
# Check that sidenav has proper role (should be navigation)
role = sidenav.get_attribute("role")
# If no explicit role, check that it's semantically correct
if not role:
# Should contain navigation links
nav_links = sidenav.find_elements(By.CSS_SELECTOR, "a[data-calc]")
assert len(nav_links) > 0
def test_mobile_nav_calculator_integration(self, calculator_page):
"""Test that mobile navigation works properly with calculator functionality"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
# Navigate to subnet calculator
calculator_page.get("http://localhost:8008/subnet")
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']"))
)
# Open mobile navigation using JavaScript
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Wait for nav to open
WebDriverWait(calculator_page, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
# Navigate to a different calculator via mobile nav
nav_link = calculator_page.find_element(By.CSS_SELECTOR, "a[data-calc='currency']")
nav_link.click()
# Wait for nav to close and currency calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='amount']"))
)
# Verify we're on the currency calculator
currency_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='amount']")
assert currency_input.is_displayed()
# Verify nav is closed
sidenav = calculator_page.find_element(By.ID, "nav")
assert "mobile-active" not in sidenav.get_attribute("class")
def test_mobile_nav_scroll_behavior(self, calculator_page):
"""Test that mobile navigation doesn't interfere with page scrolling"""
# Set mobile viewport
calculator_page.set_window_size(375, 667)
# Navigate to a calculator with long content
calculator_page.get("http://localhost:8008/subnet")
# Wait for calculator to load
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']"))
)
# Enter an IP address to generate content
ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']")
ip_input.clear()
ip_input.send_keys("10.0.0.1")
# Wait for results to appear
WebDriverWait(calculator_page, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "result"))
)
# Open mobile navigation using JavaScript
nav_toggle = calculator_page.find_element(By.ID, "navToggle")
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Wait for nav to open
WebDriverWait(calculator_page, 5).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)
# Try to scroll the page
calculator_page.execute_script("window.scrollTo(0, 100)")
# Verify navigation is still open and functional
sidenav = calculator_page.find_element(By.ID, "nav")
assert "mobile-active" in sidenav.get_attribute("class")
# Close navigation using JavaScript
calculator_page.execute_script("arguments[0].click();", nav_toggle)
# Verify navigation closes
WebDriverWait(calculator_page, 5).until_not(
EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active"))
)