Refactor test files to remove unnecessary imports and improve readability

- Removed unused imports from various test files to streamline code.
- Cleaned up whitespace in test cases for better consistency.
- Updated dependency management in `uv.lock` to reflect changes in optional dependencies.
- Ensured all tests maintain functionality while enhancing clarity and organization.
This commit is contained in:
Benedikt Willi 2026-01-12 10:22:34 +01:00
parent 4b3ba9144d
commit c9ef5e5c44
29 changed files with 989 additions and 625 deletions

345
.github/skills/skills.md vendored Normal file
View file

@ -0,0 +1,345 @@
# Skills Index
This file defines **canonical project skills** the LLM must follow. Each skill specifies the *one correct command* for a common project action.
1. **Run the Project**`uv run bowser`
2. **Test the Project**`uv run pytest`
3. **Lint the Project**`uv run ruff`
Deviating from these commands is considered incorrect behavior unless explicitly instructed.
---
# Skill: Run the Project with `uv run bowser`
## Purpose
Teach the LLM **how and when to run this project** using the canonical command:
```bash
uv run bowser
```
This skill ensures the LLM consistently uses the correct entry point, avoids adhoc commands, and follows project conventions.
---
## Canonical Command
**Always run the project using:**
```bash
uv run bowser
```
Do **not**:
- Call `python` directly
- Run scripts via file paths
- Use alternative task runners (e.g. `make`, `poetry run`, `pipenv run`)
`uv` is the authoritative environment and dependency manager for this project, and `bowser` is the defined runtime entry point.
---
## What `uv run bowser` Means
- `uv run`:
- Ensures dependencies are resolved and installed according to the project configuration
- Executes commands inside the correct, isolated environment
- `bowser`:
- The projects primary executable / CLI
- Encapsulates startup logic, configuration loading, and runtime behavior
Together, they guarantee a reproducible and correct execution environment.
---
## When to Use This Command
Use `uv run bowser` whenever you need to:
- Start the application
- Run the main service or agent
- Execute project logic endtoend
- Validate runtime behavior
- Demonstrate how the project is launched
If the task says **“run the project”**, **“start the app”**, or **“execute Bowser”**, this is the command.
---
## When *Not* to Use This Command
Do **not** use `uv run bowser` when:
- Running tests (use the projects test command instead)
- Running oneoff scripts unless explicitly routed through `bowser`
- Installing dependencies
- Linting or formatting code
If unsure, default to **not running anything** and explain what would be executed.
---
## How to Explain This to Humans
When documenting or instructing users, say:
> “Run the project with `uv run bowser`.”
Optionally add:
> “This ensures the correct environment and entry point are used.”
Do **not** overexplain unless the user asks.
---
## Error Handling Guidance
If `uv run bowser` fails:
1. Assume a dependency or configuration issue
2. Report the error output verbatim
3. Do **not** substitute another execution method
4. Suggest fixing the root cause, not changing the command
---
## Mental Model for the LLM
- There is **one** correct way to run the project
- That way is **stable and intentional**
- Deviating from it is a bug
Think of `uv run bowser` as:
> “The projects onswitch.”
---
## Summary (Checklist)
Before suggesting how to run the project, verify:
- [ ] You are using `uv run`
- [ ] You are invoking `bowser`
- [ ] You are not calling Python directly
- [ ] You are not inventing alternate commands
If all are true, you are doing it right.
---
# Skill: Test the Project with `uv run pytest`
## Purpose
Teach the LLM **how and when to run tests** for this project using the canonical command:
```bash
uv run pytest
```
This skill ensures tests are executed in the correct environment, using the projects standard tooling, without inventing alternate commands.
---
## Canonical Command
**Always run tests using:**
```bash
uv run pytest
```
Do **not**:
- Call `pytest` directly
- Use `python -m pytest`
- Run tests via ad-hoc scripts or task runners
`uv` is the authoritative environment manager, and `pytest` is the test runner for this project.
---
## What `uv run pytest` Means
- `uv run`:
- Ensures dependencies (including test dependencies) are resolved correctly
- Runs inside the same environment model as the application
- `pytest`:
- Discovers and runs the projects test suite
- Applies project-level configuration (e.g. `pytest.ini`, `pyproject.toml`)
Together, they guarantee consistent and reproducible test execution.
---
## When to Use This Command
Use `uv run pytest` whenever you need to:
- Run the full test suite
- Verify a change before or after modifying code
- Reproduce a failing test
- Validate behavior without starting the application
If the task says **“run tests”**, **“test the project”**, or **“verify with pytest”**, this is the command.
---
## When *Not* to Use This Command
Do **not** use `uv run pytest` when:
- Running the application (use `uv run bowser`)
- Installing dependencies
- Linting or formatting code
- Executing non-test scripts
If unsure, default to explaining what tests would be run rather than executing them.
---
## Error Handling Guidance
If `uv run pytest` fails:
1. Capture and report the full pytest output
2. Identify whether the failure is:
- A test assertion failure
- A missing dependency or import error
- A configuration issue
3. Do **not** change the command to work around the failure
4. Fix the underlying cause, then re-run the same command
---
## Mental Model for the LLM
- There is **one** correct way to run tests
- Test execution should mirror the real runtime environment
- Consistency matters more than convenience
Think of `uv run pytest` as:
> “The projects truth-check.”
---
## Summary (Checklist)
Before suggesting how to test the project, verify:
- [ ] You are using `uv run`
- [ ] You are invoking `pytest`
- [ ] You are not calling Python directly
- [ ] You are not inventing alternate test commands
If all are true, you are doing it right.
---
# Skill: Lint the Project with `uv run ruff`
## Purpose
Teach the LLM **how and when to lint the project** using the canonical command:
```bash
uv run ruff
```
This skill ensures linting is performed consistently, using the projects configured rules and environment.
---
## Canonical Command
**Always lint the project using:**
```bash
uv run ruff
```
Do **not**:
- Call `ruff` directly
- Use alternative linters unless explicitly instructed
- Invoke formatting or linting via ad-hoc scripts
`uv` guarantees the correct environment, and `ruff` enforces the projects linting standards.
---
## What `uv run ruff` Means
- `uv run`:
- Executes linting inside the managed project environment
- Ensures the correct version of `ruff` and dependencies are used
- `ruff`:
- Performs fast, opinionated linting
- Applies rules configured in project files (e.g. `pyproject.toml`)
Together, they provide deterministic and repeatable lint results.
---
## When to Use This Command
Use `uv run ruff` whenever you need to:
- Check code quality
- Identify linting or style issues
- Validate changes before committing
- Respond to a request to “lint the project” or “run ruff”
---
## When *Not* to Use This Command
Do **not** use `uv run ruff` when:
- Running the application (`uv run bowser`)
- Running tests (`uv run pytest`)
- Formatting code unless `ruff` is explicitly configured to do so
- Installing dependencies
If unsure, explain what linting would check instead of executing it.
---
## Error Handling Guidance
If `uv run ruff` reports issues:
1. Treat findings as authoritative
2. Report errors or warnings clearly
3. Do **not** suppress or bypass lint rules
4. Fix the code, then re-run the same command
---
## Mental Model for the LLM
- Linting enforces shared standards
- Speed and consistency matter more than flexibility
- There is **one** correct linting command
Think of `uv run ruff` as:
> “The projects code-quality gate.”
---
## Summary (Checklist)
Before suggesting how to lint the project, verify:
- [ ] You are using `uv run`
- [ ] You are invoking `ruff`
- [ ] You are not inventing alternate linting tools
If all are true, you are doing it right.

View file

@ -16,7 +16,7 @@ dependencies = [
"Jinja2>=3.0", # Template engine for pages "Jinja2>=3.0", # Template engine for pages
] ]
[project.optional-dependencies] [dependency-groups]
dev = [ dev = [
"pytest>=9.0.0", "pytest>=9.0.0",
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",
@ -35,12 +35,14 @@ managed = true
packages = ["src"] packages = ["src"]
[tool.black] [tool.black]
line-length = 100 line-length = 120
target-version = ["py311"] target-version = ["py311"]
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 120
target-version = "py311" target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "W"] select = ["E", "F", "W"]
[tool.mypy] [tool.mypy]

View file

@ -1,3 +1,4 @@
# ruff: noqa: E402
"""Browser entry and orchestration.""" """Browser entry and orchestration."""
import gi import gi

View file

@ -1,3 +1,4 @@
# ruff: noqa: E402
"""Browser chrome (Adwaita UI).""" """Browser chrome (Adwaita UI)."""
import gi import gi
@ -433,8 +434,10 @@ class Chrome:
# Store sub-timings for display # Store sub-timings for display
if self.debug_mode: if self.debug_mode:
self._render_sub_timings = sub_timings self._render_sub_timings = sub_timings
self._visible_line_count = len([l for l in self.text_layout self._visible_line_count = len([
if self.scroll_y - 50 <= l["y"] + l["font_size"] <= self.scroll_y + height + 50]) line for line in self.text_layout
if self.scroll_y - 50 <= line["y"] + line["font_size"] <= self.scroll_y + height + 50
])
def _draw_selection_highlight(self, canvas, width: int): def _draw_selection_highlight(self, canvas, width: int):
"""Draw selection highlight rectangle.""" """Draw selection highlight rectangle."""
@ -531,7 +534,13 @@ class Chrome:
if self._render_sub_timings: if self._render_sub_timings:
y += 16 y += 16
text_paint.setColor(skia.Color(150, 200, 255, 255)) text_paint.setColor(skia.Color(150, 200, 255, 255))
canvas.drawString(f"render_dom breakdown ({self._visible_line_count} lines):", panel_x + 5, y, small_font, text_paint) canvas.drawString(
f"render_dom breakdown ({self._visible_line_count} lines):",
panel_x + 5,
y,
small_font,
text_paint,
)
sub_sorted = sorted(self._render_sub_timings.items(), key=lambda x: x[1], reverse=True) sub_sorted = sorted(self._render_sub_timings.items(), key=lambda x: x[1], reverse=True)
for name, duration in sub_sorted: for name, duration in sub_sorted:
@ -750,7 +759,7 @@ class Chrome:
line_bottom = line_info["y"] + line_info["height"] line_bottom = line_info["y"] + line_info["height"]
line_left = line_info["x"] line_left = line_info["x"]
char_positions = line_info.get("char_positions", []) char_positions = line_info.get("char_positions", [])
text = line_info["text"] # text not needed for highlight geometry
# Skip lines completely outside selection # Skip lines completely outside selection
if line_bottom < sel_start[1] or line_top > sel_end[1]: if line_bottom < sel_start[1] or line_top > sel_end[1]:
@ -764,13 +773,21 @@ class Chrome:
if line_top <= sel_start[1] < line_bottom: if line_top <= sel_start[1] < line_bottom:
# Find character index at sel_start x # Find character index at sel_start x
start_char_idx = self._x_to_char_index(sel_start[0], line_left, char_positions) start_char_idx = self._x_to_char_index(sel_start[0], line_left, char_positions)
hl_left = line_left + char_positions[start_char_idx] if start_char_idx < len(char_positions) else line_left hl_left = (
line_left + char_positions[start_char_idx]
if start_char_idx < len(char_positions)
else line_left
)
# If this line contains the end of selection # If this line contains the end of selection
if line_top <= sel_end[1] < line_bottom: if line_top <= sel_end[1] < line_bottom:
# Find character index at sel_end x # Find character index at sel_end x
end_char_idx = self._x_to_char_index(sel_end[0], line_left, char_positions) end_char_idx = self._x_to_char_index(sel_end[0], line_left, char_positions)
hl_right = line_left + char_positions[end_char_idx] if end_char_idx < len(char_positions) else hl_right hl_right = (
line_left + char_positions[end_char_idx]
if end_char_idx < len(char_positions)
else hl_right
)
# Draw highlight # Draw highlight
if hl_right > hl_left: if hl_right > hl_left:

View file

@ -1,6 +1,6 @@
"""Tab and frame orchestration stubs.""" """Tab and frame orchestration stubs."""
from typing import Optional from typing import Optional, TYPE_CHECKING
import logging import logging
from ..network.url import URL from ..network.url import URL
@ -8,6 +8,9 @@ from ..network import http
from ..parser.html import parse_html, Element from ..parser.html import parse_html, Element
from ..templates import render_startpage, render_error_page from ..templates import render_startpage, render_error_page
if TYPE_CHECKING:
from .browser import Browser
class Frame: class Frame:
def __init__(self, tab: "Tab", parent_frame=None, frame_element=None): def __init__(self, tab: "Tab", parent_frame=None, frame_element=None):

View file

@ -2,7 +2,6 @@
from ..parser.html import Element, Text from ..parser.html import Element, Text
from ..render.fonts import get_font, linespace from ..render.fonts import get_font, linespace
from .block import BlockLayout, LineLayout
class LayoutLine: class LayoutLine:

View file

@ -1,6 +1,6 @@
"""Inline and text layout.""" """Inline and text layout."""
from ..render.fonts import get_font, measure_text, linespace from ..render.fonts import get_font, linespace
class TextLayout: class TextLayout:

View file

@ -7,7 +7,12 @@ import logging
from .url import URL from .url import URL
def request(url: URL, payload: Optional[bytes] = None, method: str = "GET", max_redirects: int = 10) -> Tuple[int, str, bytes]: def request(
url: URL,
payload: Optional[bytes] = None,
method: str = "GET",
max_redirects: int = 10,
) -> Tuple[int, str, bytes]:
""" """
Fetch a URL and follow redirects, returning (status_code, content_type, body). Fetch a URL and follow redirects, returning (status_code, content_type, body).

View file

@ -5,7 +5,7 @@ from typing import Optional
from ..parser.html import Element from ..parser.html import Element
from ..layout.document import DocumentLayout from ..layout.document import DocumentLayout
from .fonts import get_font from .fonts import get_font
from .paint import DisplayList, DrawText, DrawRect from .paint import DisplayList
class RenderPipeline: class RenderPipeline:

View file

@ -1,6 +1,5 @@
"""Template rendering utilities.""" """Template rendering utilities."""
import os
from pathlib import Path from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
import logging import logging
@ -66,13 +65,7 @@ def render_error_page(status_code: int, url: str = "", error_message: str = "")
""" """
logger = logging.getLogger("bowser.templates") logger = logging.getLogger("bowser.templates")
# Map common status codes to templates # Determine template per status
template_map = {
404: "error_404.html",
500: "error_500.html",
# Network errors
"network": "error_network.html",
}
if status_code == 404: if status_code == 404:
template = "error_404.html" template = "error_404.html"

View file

@ -1,6 +1,5 @@
"""Tests for browser tab management.""" """Tests for browser tab management."""
import pytest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from src.browser.browser import Browser from src.browser.browser import Browser
from src.browser.tab import Tab from src.browser.tab import Tab
@ -140,7 +139,7 @@ class TestBrowser:
browser.chrome.tabs_box = Mock() browser.chrome.tabs_box = Mock()
tab1 = browser.new_tab("https://example.com") tab1 = browser.new_tab("https://example.com")
tab2 = browser.new_tab("https://other.com") _ = browser.new_tab("https://other.com")
browser.set_active_tab(tab1) browser.set_active_tab(tab1)
assert browser.active_tab is tab1 assert browser.active_tab is tab1
@ -168,7 +167,7 @@ class TestBrowser:
browser.chrome.paint = Mock() browser.chrome.paint = Mock()
browser.chrome.tabs_box = Mock() browser.chrome.tabs_box = Mock()
tab1 = browser.new_tab("https://example.com") _ = browser.new_tab("https://example.com")
tab2 = browser.new_tab("https://other.com") tab2 = browser.new_tab("https://other.com")
tab3 = browser.new_tab("https://third.com") tab3 = browser.new_tab("https://third.com")

View file

@ -1,6 +1,5 @@
"""Tests for cookie management.""" """Tests for cookie management."""
import pytest
from src.network.cookies import CookieJar from src.network.cookies import CookieJar

View file

@ -1,7 +1,6 @@
"""Tests for DOM graph visualization.""" """Tests for DOM graph visualization."""
import pytest from src.parser.html import parse_html
from src.parser.html import parse_html, Element, Text
from src.debug.dom_graph import generate_dot_graph, print_dom_tree from src.debug.dom_graph import generate_dot_graph, print_dom_tree
@ -93,8 +92,8 @@ class TestDOMGraph:
# Should have increasing indentation # Should have increasing indentation
lines = tree.split('\n') lines = tree.split('\n')
# Find the nested <p> line - should be more indented than <div> # Find the nested <p> line - should be more indented than <div>
p_line = [l for l in lines if '<p>' in l][0] p_line = [line for line in lines if '<p>' in line][0]
div_line = [l for l in lines if '<div>' in l][0] div_line = [line for line in lines if '<div>' in line][0]
# Count leading spaces # Count leading spaces
p_indent = len(p_line) - len(p_line.lstrip()) p_indent = len(p_line) - len(p_line.lstrip())

View file

@ -1,8 +1,6 @@
"""Tests for DOM graph page rendering.""" """Tests for DOM graph page rendering."""
import pytest
from src.templates import render_dom_graph_page from src.templates import render_dom_graph_page
from pathlib import Path
import tempfile import tempfile
import os import os

View file

@ -1,8 +1,7 @@
"""Tests for Frame and content loading.""" """Tests for Frame and content loading."""
import pytest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from src.browser.tab import Frame, Tab from src.browser.tab import Tab
from src.network.url import URL from src.network.url import URL

View file

@ -1,6 +1,5 @@
"""Tests for HTML parsing functionality.""" """Tests for HTML parsing functionality."""
import pytest
from src.parser.html import parse_html, Text, Element from src.parser.html import parse_html, Text, Element

View file

@ -1,7 +1,7 @@
"""Tests for HTTP functionality.""" """Tests for HTTP functionality."""
import pytest import pytest
from unittest.mock import Mock, patch, MagicMock from unittest.mock import Mock, patch
from src.network.url import URL from src.network.url import URL
from src.network import http from src.network import http

View file

@ -1,8 +1,9 @@
# ruff: noqa: E402
"""Tests for layout components.""" """Tests for layout components."""
import pytest import pytest
import sys import sys
from unittest.mock import Mock, patch, MagicMock from unittest.mock import MagicMock
# Mock skia before importing layout modules # Mock skia before importing layout modules
mock_skia = MagicMock() mock_skia = MagicMock()
@ -131,7 +132,12 @@ class TestDocumentLayout:
body = Element("body") body = Element("body")
p = Element("p") p = Element("p")
# Long text that should wrap # Long text that should wrap
p.children = [Text("This is a very long paragraph that should wrap to multiple lines when the width is narrow enough")] p.children = [
Text(
"This is a very long paragraph that should wrap to multiple lines "
"when the width is narrow enough"
)
]
body.children = [p] body.children = [p]
layout = DocumentLayout(body) layout = DocumentLayout(body)

View file

@ -1,6 +1,5 @@
"""Tests for HTML parsing.""" """Tests for HTML parsing."""
import pytest
from src.parser.html import Text, Element, print_tree from src.parser.html import Text, Element, print_tree

View file

@ -1,8 +1,9 @@
# ruff: noqa: E402
"""Tests for rendering primitives.""" """Tests for rendering primitives."""
import pytest import pytest
import sys import sys
from unittest.mock import Mock, patch, MagicMock from unittest.mock import MagicMock
# Mock skia before importing render modules # Mock skia before importing render modules
mock_skia = MagicMock() mock_skia = MagicMock()

View file

@ -1,6 +1,5 @@
"""Tests for template rendering.""" """Tests for template rendering."""
import pytest
from src.templates import render_template, render_error_page, render_startpage from src.templates import render_template, render_error_page, render_startpage

View file

@ -1,6 +1,5 @@
"""Tests for URL parsing and resolution.""" """Tests for URL parsing and resolution."""
import pytest
from src.network.url import URL from src.network.url import URL

View file

@ -1,6 +1,5 @@
"""Tests for URL normalization.""" """Tests for URL normalization."""
import pytest
from src.browser.browser import Browser from src.browser.browser import Browser

17
uv.lock
View file

@ -49,7 +49,7 @@ dependencies = [
{ name = "skia-python" }, { name = "skia-python" },
] ]
[package.optional-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "black" }, { name = "black" },
{ name = "mypy" }, { name = "mypy" },
@ -60,16 +60,19 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "black", marker = "extra == 'dev'", specifier = ">=25.0" },
{ name = "jinja2", specifier = ">=3.0" }, { name = "jinja2", specifier = ">=3.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.9.0" },
{ name = "pygobject", specifier = ">=3.54.0" }, { name = "pygobject", specifier = ">=3.54.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.0" },
{ name = "skia-python", specifier = ">=87.9" }, { name = "skia-python", specifier = ">=87.9" },
] ]
provides-extras = ["dev"]
[package.metadata.requires-dev]
dev = [
{ name = "black", specifier = ">=25.0" },
{ name = "mypy", specifier = ">=1.9.0" },
{ name = "pytest", specifier = ">=9.0.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.9.0" },
]
[[package]] [[package]]
name = "click" name = "click"