mobile responsiveness
This commit is contained in:
parent
b93349aa4d
commit
ad664c32ea
4 changed files with 494 additions and 3 deletions
|
@ -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 ---- */
|
||||
|
|
|
@ -14,8 +14,15 @@
|
|||
<header class="bar">
|
||||
<div class="wrap bar__inner">
|
||||
<div class="brand">calculator.127local.net</div>
|
||||
<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>
|
||||
|
||||
<div class="wrap layout">
|
||||
|
|
|
@ -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
344
tests/test_mobile.py
Normal 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"))
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue