mirror of
https://github.com/Hopiu/bowser.git
synced 2026-03-16 19:10:24 +00:00
Add automatic HTTPS protocol assumption for URLs
Feature: Smart protocol handling - If no protocol is provided, assume https:// - URLs like 'example.com' become 'https://example.com' - 'example.com/path' becomes 'https://example.com/path' - Special 'about:' URLs are preserved as-is Implementation: - Added _normalize_url() method to Browser class - Checks for '://' in URL to detect existing protocol - Strips whitespace from URLs - Applied in both new_tab() and navigate_to() methods - Supports all URL formats (subdomains, ports, paths, queries) URL Normalization Logic: 1. Strip leading/trailing whitespace 2. Check if URL already has a protocol ('://') 3. Check for special about: URLs 4. Otherwise prepend 'https://' Examples: - 'example.com' → 'https://example.com' - 'https://example.com' → 'https://example.com' (unchanged) - 'about:startpage' → 'about:startpage' (unchanged) - 'www.example.com:8080' → 'https://www.example.com:8080' - 'localhost:3000' → 'https://localhost:3000' Tests added (10 test cases): - test_normalize_url_with_https - test_normalize_url_with_http - test_normalize_url_without_protocol - test_normalize_url_with_path - test_normalize_url_with_about - test_normalize_url_strips_whitespace - test_normalize_url_with_query_string - test_normalize_url_with_subdomain - test_normalize_url_with_port - test_normalize_url_localhost Existing tests still passing (15/15)
This commit is contained in:
parent
cb6103ce04
commit
d3119f0b10
6 changed files with 166 additions and 32 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -33,6 +33,8 @@ class Browser:
|
|||
|
||||
def new_tab(self, url: str):
|
||||
tab = Tab(self)
|
||||
# Normalize URL to ensure https:// protocol
|
||||
url = self._normalize_url(url)
|
||||
tab.load(URL(url))
|
||||
self.tabs.append(tab)
|
||||
self.active_tab = tab
|
||||
|
|
@ -76,6 +78,8 @@ class Browser:
|
|||
def navigate_to(self, url_str: str):
|
||||
if not url_str:
|
||||
return
|
||||
# Add https:// if no protocol provided
|
||||
url_str = self._normalize_url(url_str)
|
||||
if not self.active_tab:
|
||||
self.new_tab(url_str)
|
||||
return
|
||||
|
|
@ -84,6 +88,18 @@ class Browser:
|
|||
self.chrome.update_address_bar()
|
||||
self._log(f"Navigate to: {url_str}", logging.INFO)
|
||||
|
||||
def _normalize_url(self, url_str: str) -> str:
|
||||
"""Add https:// protocol if not present."""
|
||||
url_str = url_str.strip()
|
||||
# If URL already has a protocol, return as-is
|
||||
if "://" in url_str:
|
||||
return url_str
|
||||
# Special about: URLs
|
||||
if url_str.startswith("about:"):
|
||||
return url_str
|
||||
# Otherwise, assume https://
|
||||
return f"https://{url_str}"
|
||||
|
||||
def go_back(self):
|
||||
if self.active_tab and self.active_tab.go_back():
|
||||
self.chrome.paint()
|
||||
|
|
|
|||
|
|
@ -133,40 +133,13 @@ class Chrome:
|
|||
# Clear existing tabs
|
||||
self._clear_children(self.tabs_box)
|
||||
|
||||
# Add each tab as a simple button
|
||||
# Add each tab as an integrated unit
|
||||
for i, tab in enumerate(self.browser.tabs):
|
||||
is_active = tab is self.browser.active_tab
|
||||
|
||||
# Simple container for tab label + close button
|
||||
tab_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
tab_box.set_homogeneous(False)
|
||||
|
||||
# Tab label button
|
||||
tab_label = f"{i+1}: {tab.title}"
|
||||
tab_btn = Gtk.Button(label=tab_label)
|
||||
tab_btn.set_hexpand(True)
|
||||
tab_btn.set_relief(Gtk.ReliefStyle.NORMAL)
|
||||
|
||||
if is_active:
|
||||
tab_btn.add_css_class("suggested-action")
|
||||
else:
|
||||
tab_btn.add_css_class("flat")
|
||||
|
||||
# Store tab reference on the button for handler
|
||||
tab_btn.tab = tab
|
||||
tab_btn.connect("clicked", self._on_tab_clicked)
|
||||
tab_box.append(tab_btn)
|
||||
|
||||
# Close button
|
||||
close_btn = Gtk.Button(label="✕")
|
||||
close_btn.set_size_request(36, -1)
|
||||
close_btn.set_relief(Gtk.ReliefStyle.FLAT)
|
||||
close_btn.add_css_class("flat")
|
||||
close_btn.tab = tab
|
||||
close_btn.connect("clicked", self._on_close_clicked)
|
||||
tab_box.append(close_btn)
|
||||
|
||||
self.tabs_box.append(tab_box)
|
||||
# Create integrated tab widget
|
||||
tab_widget = self._create_integrated_tab(tab, i, is_active)
|
||||
self.tabs_box.append(tab_widget)
|
||||
|
||||
# New tab button
|
||||
new_tab_btn = Gtk.Button(label="+")
|
||||
|
|
@ -176,6 +149,80 @@ class Chrome:
|
|||
new_tab_btn.connect("clicked", self._on_new_tab_clicked)
|
||||
self.tabs_box.append(new_tab_btn)
|
||||
|
||||
def _create_integrated_tab(self, tab, index: int, is_active: bool) -> Gtk.Widget:
|
||||
"""Create an integrated tab widget with close button as one unit."""
|
||||
# Outer container - this is the visual tab
|
||||
tab_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
|
||||
tab_container.add_css_class("integrated-tab")
|
||||
if is_active:
|
||||
tab_container.add_css_class("active-tab")
|
||||
tab_container.set_homogeneous(False)
|
||||
|
||||
# Left side: tab label (expandable)
|
||||
tab_label = f"{index+1}: {tab.title}"
|
||||
tab_btn = Gtk.Button(label=tab_label)
|
||||
tab_btn.set_hexpand(True)
|
||||
tab_btn.add_css_class("tab-label")
|
||||
tab_btn.add_css_class("flat")
|
||||
tab_btn.tab = tab
|
||||
tab_btn.connect("clicked", self._on_tab_clicked)
|
||||
tab_container.append(tab_btn)
|
||||
|
||||
# Right side: close button (fixed width, no expand)
|
||||
close_btn = Gtk.Button(label="✕")
|
||||
close_btn.set_size_request(34, -1)
|
||||
close_btn.add_css_class("tab-close")
|
||||
close_btn.add_css_class("flat")
|
||||
close_btn.tab = tab
|
||||
close_btn.connect("clicked", self._on_close_clicked)
|
||||
tab_container.append(close_btn)
|
||||
|
||||
# Apply CSS styling to make it look like one unit
|
||||
css = Gtk.CssProvider()
|
||||
css.load_from_data(b"""
|
||||
.integrated-tab {
|
||||
background-color: @theme_bg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 4px 4px 0 0;
|
||||
margin-right: 2px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.integrated-tab.active-tab {
|
||||
background-color: @theme_base_color;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
padding: 6px 8px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.tab-label:hover {
|
||||
background-color: mix(@theme_bg_color, @theme_fg_color, 0.95);
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
padding: 2px 4px;
|
||||
font-size: 0.9em;
|
||||
border: none;
|
||||
border-left: 1px solid @borders;
|
||||
border-radius: 0;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background-color: @error_color;
|
||||
color: white;
|
||||
}
|
||||
""")
|
||||
|
||||
context = tab_container.get_style_context()
|
||||
context.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
|
||||
return tab_container
|
||||
|
||||
def _on_tab_clicked(self, btn: Gtk.Button):
|
||||
"""Handle tab button click - set as active."""
|
||||
if hasattr(btn, 'tab'):
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ class Frame:
|
|||
logger = logging.getLogger("bowser.frame")
|
||||
|
||||
# Handle special about: URLs
|
||||
if url.origin == "about:startpage":
|
||||
url_str = str(url)
|
||||
if url_str.startswith("about:startpage"):
|
||||
html = render_startpage()
|
||||
self.document = parse_html(html)
|
||||
self.tab.current_url = url
|
||||
|
|
|
|||
70
tests/test_url_normalization.py
Normal file
70
tests/test_url_normalization.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Tests for URL normalization."""
|
||||
|
||||
import pytest
|
||||
from src.browser.browser import Browser
|
||||
|
||||
|
||||
class TestURLNormalization:
|
||||
def setup_method(self):
|
||||
"""Create a browser instance for each test."""
|
||||
self.browser = Browser()
|
||||
|
||||
def test_normalize_url_with_https(self):
|
||||
"""Test that URLs with https:// protocol are unchanged."""
|
||||
url = "https://example.com"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://example.com"
|
||||
|
||||
def test_normalize_url_with_http(self):
|
||||
"""Test that URLs with http:// protocol are unchanged."""
|
||||
url = "http://example.com"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "http://example.com"
|
||||
|
||||
def test_normalize_url_without_protocol(self):
|
||||
"""Test that URLs without protocol get https:// added."""
|
||||
url = "example.com"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://example.com"
|
||||
|
||||
def test_normalize_url_with_path(self):
|
||||
"""Test that URLs with path but no protocol get https:// added."""
|
||||
url = "example.com/path/to/page"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://example.com/path/to/page"
|
||||
|
||||
def test_normalize_url_with_about(self):
|
||||
"""Test that about: URLs are not modified."""
|
||||
url = "about:startpage"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "about:startpage"
|
||||
|
||||
def test_normalize_url_strips_whitespace(self):
|
||||
"""Test that leading/trailing whitespace is stripped."""
|
||||
url = " example.com "
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://example.com"
|
||||
|
||||
def test_normalize_url_with_query_string(self):
|
||||
"""Test that URLs with query strings work correctly."""
|
||||
url = "example.com/search?q=test"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://example.com/search?q=test"
|
||||
|
||||
def test_normalize_url_with_subdomain(self):
|
||||
"""Test that subdomains work correctly."""
|
||||
url = "www.example.com"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://www.example.com"
|
||||
|
||||
def test_normalize_url_with_port(self):
|
||||
"""Test that ports are preserved."""
|
||||
url = "example.com:8080"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://example.com:8080"
|
||||
|
||||
def test_normalize_url_localhost(self):
|
||||
"""Test that localhost URLs work correctly."""
|
||||
url = "localhost:3000"
|
||||
normalized = self.browser._normalize_url(url)
|
||||
assert normalized == "https://localhost:3000"
|
||||
Loading…
Reference in a new issue