Skip to content

Commit ea77631

Browse files
joshribakoffclaude
andcommitted
Fix footer, test isolation, cleanup obsolete tests
- Use Textual built-in Footer widget (fixes footer not showing) - Add view indicator to title bar (Work/Plans) - Show w/p keybindings in footer - Isolate tests from user's session file via monkeypatch - Remove obsolete modal tests (now fullscreen views) - Attempt to fix selection preservation (partial - async timing issue) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4347254 commit ea77631

File tree

8 files changed

+125
-233
lines changed

8 files changed

+125
-233
lines changed

tui/bearing_tui/app.py

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class BearingApp(App):
197197
"""Bearing worktree management TUI."""
198198

199199
CSS_PATH = "styles/app.tcss"
200-
TITLE = "⚓ Bearing [dev-check]"
200+
TITLE = "⚓ Bearing "
201201

202202
BINDINGS = [
203203
Binding("q", "quit", "Quit"),
@@ -210,8 +210,8 @@ class BearingApp(App):
210210
Binding("d", "daemon", "Daemon"),
211211
Binding("o", "open_item", "Open", show=False),
212212
# View switching
213-
Binding("w", "switch_to_worktrees", "Worktrees", show=False),
214-
Binding("p", "switch_to_plans", "Plans", show=False),
213+
Binding("w", "switch_to_worktrees", "Work"),
214+
Binding("p", "switch_to_plans", "Plans"),
215215
# Panel navigation by number (0-indexed)
216216
Binding("0", "focus_panel_0", "Projects", show=False),
217217
Binding("1", "focus_panel_1", "Main", show=False),
@@ -246,104 +246,84 @@ def __init__(self, workspace: Path | None = None):
246246

247247
def compose(self) -> ComposeResult:
248248
"""Create the app layout."""
249-
yield Static("\u2693 Bearing [dev-check]", id="title")
249+
yield Static(" Bearing - Work", id="title")
250250
with Horizontal(id="main-container"):
251251
with Vertical(id="projects-panel"):
252-
yield Label("[0] Projects [dev-check]", classes="panel-header")
252+
yield Label("[0] Projects ", classes="panel-header")
253253
yield ProjectList(id="project-list")
254254
with Vertical(id="main-panel"):
255-
yield Label("[1] Worktrees [dev-check]", classes="panel-header", id="main-panel-header")
255+
yield Label("[1] Worktrees ", classes="panel-header", id="main-panel-header")
256256
yield WorktreeTable(id="worktree-table")
257257
yield PlansTable(id="plans-table")
258258
yield Label("[2] Details", classes="panel-header details-header")
259259
yield DetailsPanel(id="details-panel")
260-
yield Static(self._get_footer_text(), id="footer-bar")
261-
262-
def _get_footer_text(self) -> str:
263-
"""Get footer text with current mode highlighted."""
264-
if self._view_mode == ViewMode.WORKTREES:
265-
mode_text = "[bold cyan][w]orktrees[/] [dim][p]lans[/]"
266-
else:
267-
mode_text = "[dim][w]orktrees[/] [bold cyan][p]lans[/]"
268-
return (
269-
f"{mode_text} "
270-
"[yellow]j/k[/] nav "
271-
"[yellow]o[/]pen "
272-
"[yellow]r[/]efresh "
273-
"[yellow]R[/] PRs "
274-
"[yellow]p[/]lans "
275-
"[yellow]?[/] help "
276-
"[yellow]q[/]uit"
277-
)
278-
279-
def _update_footer(self) -> None:
280-
"""Update footer with current mode."""
281-
footer = self.query_one("#footer-bar", Static)
282-
footer.update(self._get_footer_text())
260+
yield Footer()
283261

284262
def _update_view(self) -> None:
285263
"""Update UI for current view mode."""
286264
worktree_table = self.query_one("#worktree-table", WorktreeTable)
287265
plans_table = self.query_one("#plans-table", PlansTable)
288266
header = self.query_one("#main-panel-header", Label)
267+
title = self.query_one("#title", Static)
289268

290269
if self._view_mode == ViewMode.WORKTREES:
270+
title.update("⚓ Bearing - Work")
291271
worktree_table.display = True
292272
plans_table.display = False
293273
header.update("[1] Worktrees")
294274
self._panel_order = ["project-list", "worktree-table", "details-panel"]
295275
else:
276+
title.update("⚓ Bearing - Plans")
296277
worktree_table.display = False
297278
plans_table.display = True
298279
header.update("[1] Plans")
299280
self._panel_order = ["project-list", "plans-table", "details-panel"]
300281

301-
self._update_footer()
302-
303282
# Update project list for current view
304283
if self._view_mode == ViewMode.WORKTREES:
305284
self._refresh_worktrees_view()
306285
else:
307286
self._refresh_plans_view()
308287

309288
def _refresh_worktrees_view(self) -> None:
310-
"""Refresh project list with worktree counts."""
289+
"""Refresh project list with worktree counts, preserving selection."""
311290
projects = self.state.get_projects()
312291
local_entries = self.state.read_local()
313292
counts: dict[str, int] = {}
314293
for entry in local_entries:
315294
counts[entry.repo] = counts.get(entry.repo, 0) + 1
316295
project_list = self.query_one(ProjectList)
317-
project_list.set_projects(projects, counts)
296+
project_list.set_projects(projects, counts, preserve_selection=self._current_project)
318297

319298
def _refresh_plans_view(self) -> None:
320-
"""Refresh project list with plan counts."""
321-
# Get projects that have plans
299+
"""Refresh project list with plan counts, preserving selection."""
322300
plan_projects = self.state.get_plan_projects()
323301
counts: dict[str, int] = {}
324302
for project in plan_projects:
325303
plans = self.state.get_plans_for_project(project)
326304
counts[project] = len(plans)
327305
project_list = self.query_one(ProjectList)
328-
project_list.set_projects(plan_projects, counts)
306+
project_list.set_projects(plan_projects, counts, preserve_selection=self._current_project)
329307

330308
def action_switch_to_worktrees(self) -> None:
331309
"""Switch to worktrees view."""
332310
if self._view_mode != ViewMode.WORKTREES:
333311
self._view_mode = ViewMode.WORKTREES
334312
self._update_view()
335-
# If project selected, load its worktrees
313+
# If project selected, load its worktrees and focus panel 1
336314
if self._current_project:
337315
self._update_worktree_table(self._current_project)
316+
self.query_one(WorktreeTable).focus()
338317

339318
def action_switch_to_plans(self) -> None:
340319
"""Switch to plans view."""
341320
if self._view_mode != ViewMode.PLANS:
342321
self._view_mode = ViewMode.PLANS
343322
self._update_view()
344-
# If project selected, load its plans
323+
# If project selected, load its plans and focus panel 1
345324
if self._current_project:
346325
self._update_plans_table(self._current_project)
326+
self.query_one(PlansTable).focus()
347327

348328
@property
349329
def _session_file(self) -> Path:
@@ -482,16 +462,18 @@ def action_show_prs(self) -> None:
482462
self.push_screen(PRsScreen(self.workspace, self.state))
483463

484464
def action_refresh(self) -> None:
485-
"""Refresh data for current view."""
465+
"""Refresh data for current view, preserving selection."""
466+
saved_project = self._current_project
467+
486468
if self._view_mode == ViewMode.WORKTREES:
487469
self._refresh_worktrees_view()
488-
self.query_one(WorktreeTable).clear_worktrees()
470+
if saved_project:
471+
self._update_worktree_table(saved_project)
489472
else:
490473
self._refresh_plans_view()
491-
self.query_one(PlansTable).clear_plans()
474+
if saved_project:
475+
self._update_plans_table(saved_project)
492476

493-
self.query_one(DetailsPanel).clear()
494-
self._current_project = None
495477
self.notify("Data refreshed", timeout=2)
496478

497479
def action_focus_panel_0(self) -> None:
@@ -546,8 +528,10 @@ def on_project_list_project_selected(self, event: ProjectList.ProjectSelected) -
546528
self._current_project = event.project
547529
if self._view_mode == ViewMode.WORKTREES:
548530
self._update_worktree_table(event.project)
531+
self.query_one(WorktreeTable).focus()
549532
else:
550533
self._update_plans_table(event.project)
534+
self.query_one(PlansTable).focus()
551535

552536
def _update_worktree_table(self, project: str) -> None:
553537
"""Update worktree table for selected project."""

tui/bearing_tui/styles/app.tcss

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,7 @@ Screen {
8383
padding: 0 1;
8484
}
8585

86-
/* Footer with keybindings */
87-
#footer-bar {
88-
width: 100%;
89-
height: 1;
90-
background: $bg-surface;
91-
color: $text-dim;
92-
padding: 0 1;
93-
border-top: solid $border;
94-
}
86+
/* Footer uses Textual's built-in Footer widget */
9587

9688
/* Project list styling */
9789
ProjectList {

tui/bearing_tui/widgets/projects.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,17 @@ def set_projects(self, projects: list[str], counts: dict[str, int] | None = None
6767
if not projects:
6868
self.append(ListItem(Static("No projects found")))
6969
else:
70+
selection_index = None
7071
for i, project in enumerate(projects):
7172
count = self._counts.get(project, 0)
72-
self.append(ProjectListItem(project, count))
73-
# Restore selection if this is the preserved project
73+
item = ProjectListItem(project, count)
74+
self.append(item)
75+
# Track selection for restoration after all items added
7476
if preserve_selection and project == preserve_selection:
75-
self.index = i
77+
selection_index = i
78+
# Restore selection after all items are added
79+
if selection_index is not None:
80+
self.index = selection_index
81+
# Manually set highlighted since watch_index may not fire correctly
82+
if self.highlighted_child:
83+
self.highlighted_child.highlighted = True

tui/tests/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
"""Pytest fixtures for Bearing TUI tests."""
22
import shutil
3+
from pathlib import Path
34

45
import pytest
56

7+
8+
@pytest.fixture(autouse=True)
9+
def isolate_session(tmp_path, monkeypatch):
10+
"""Isolate session file to temp directory to avoid affecting real user state.
11+
12+
pytest's tmp_path fixture creates a UNIQUE temp directory per test, e.g.:
13+
/tmp/pytest-of-user/pytest-123/test_foo0/
14+
/tmp/pytest-of-user/pytest-123/test_bar0/
15+
So tests won't clobber each other.
16+
"""
17+
# Create temp .bearing directory in this test's unique tmp_path
18+
temp_bearing = tmp_path / ".bearing"
19+
temp_bearing.mkdir()
20+
21+
# Monkeypatch Path.home() to return this test's temp directory
22+
# This ensures tests don't read/write the user's real ~/.bearing/
23+
def mock_home():
24+
return tmp_path
25+
26+
monkeypatch.setattr(Path, "home", staticmethod(mock_home))
27+
yield
28+
629
from tests.mock_data import (
730
create_normal_workspace,
831
create_empty_workspace,

tui/tests/test_app.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,9 @@ async def test_view_switching_to_worktrees(workspace):
342342

343343
@pytest.mark.asyncio
344344
async def test_view_switch_preserves_project_selection(workspace):
345-
"""Test that project selection persists when switching between views."""
345+
"""Test that project selection AND focus persist when switching views."""
346346
from bearing_tui.app import ViewMode
347+
from bearing_tui.widgets import PlansTable
347348

348349
# Create plans directory
349350
plans_dir = workspace / "plans" / "myapp"
@@ -352,15 +353,15 @@ async def test_view_switch_preserves_project_selection(workspace):
352353

353354
app = BearingApp(workspace=workspace)
354355
async with app.run_test() as pilot:
355-
# Select a project
356-
project_list = app.query_one(ProjectList)
356+
# Select a project (this auto-focuses worktree table)
357357
await pilot.press("j") # Move to first project
358358
await pilot.press("enter")
359359
await pilot.pause()
360360

361-
# Verify project is selected
361+
# Verify project is selected and worktree table is focused
362362
assert app._current_project is not None
363363
selected_project = app._current_project
364+
assert app.focused.id == "worktree-table"
364365

365366
# Switch to plans view
366367
await pilot.press("p")
@@ -369,6 +370,8 @@ async def test_view_switch_preserves_project_selection(workspace):
369370
# Project selection should persist
370371
assert app._current_project == selected_project
371372
assert app._view_mode == ViewMode.PLANS
373+
# Focus should be on plans table (panel 1), NOT project list (panel 0)
374+
assert app.focused.id == "plans-table"
372375

373376
# Switch back to worktrees
374377
await pilot.press("w")
@@ -377,31 +380,18 @@ async def test_view_switch_preserves_project_selection(workspace):
377380
# Project selection should still persist
378381
assert app._current_project == selected_project
379382
assert app._view_mode == ViewMode.WORKTREES
383+
# Focus should be on worktree table (panel 1)
384+
assert app.focused.id == "worktree-table"
380385

381386

382387
@pytest.mark.asyncio
383-
async def test_footer_shows_current_mode(workspace):
384-
"""Test that footer highlights current mode."""
385-
from bearing_tui.app import ViewMode
388+
async def test_footer_widget_exists(workspace):
389+
"""Test that footer widget is present."""
390+
from textual.widgets import Footer
386391

387392
app = BearingApp(workspace=workspace)
388393
async with app.run_test() as pilot:
389-
# In worktrees mode
390-
assert app._view_mode == ViewMode.WORKTREES
391-
392-
# The _get_footer_text method should return text with worktrees highlighted
393-
footer_text = app._get_footer_text()
394-
assert "[bold cyan][w]orktrees[/]" in footer_text
395-
assert "[dim][p]lans[/]" in footer_text
396-
397-
# Switch to plans
398-
await pilot.press("p")
399394
await pilot.pause()
400-
401-
# In plans mode
402-
assert app._view_mode == ViewMode.PLANS
403-
404-
# Footer should now highlight plans
405-
footer_text = app._get_footer_text()
406-
assert "[dim][w]orktrees[/]" in footer_text
407-
assert "[bold cyan][p]lans[/]" in footer_text
395+
footer = app.query_one(Footer)
396+
assert footer is not None
397+
assert footer.display is True

tui/tests/test_comprehensive.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,31 @@ async def test_panel_0_to_1_selection(workspace):
191191
assert worktree_table.row_count > 0
192192

193193

194+
@pytest.mark.asyncio
195+
async def test_project_selection_auto_focuses_worktrees(workspace):
196+
"""Test that selecting a project automatically focuses the worktree panel."""
197+
app = BearingApp(workspace=workspace)
198+
async with app.run_test(size=(100, 25)) as pilot:
199+
await pilot.pause()
200+
201+
# Start on project list
202+
await pilot.press("0")
203+
await pilot.pause()
204+
assert app.focused.id == "project-list"
205+
206+
# Select a project with Enter
207+
await pilot.press("j")
208+
await pilot.press("enter")
209+
await pilot.pause()
210+
211+
# Should AUTO-focus worktree table (no manual "1" press needed)
212+
assert app.focused.id == "worktree-table"
213+
214+
# And worktrees should be populated
215+
worktree_table = app.query_one(WorktreeTable)
216+
assert worktree_table.row_count > 0
217+
218+
194219
# ============================================================================
195220
# Empty States
196221
# ============================================================================

0 commit comments

Comments
 (0)