feat: enhance filetree pane interaction and add tests

- Improved mouse event handling in the filetree pane to correctly account for content offsets, ensuring proper scrolling and item selection.
- Added comprehensive unit tests for the filetree pane, covering various states including empty tree, tree with items, focused state, and different dimensions.
- Updated tree traversal logic to improve visual representation of nodes.
- Introduced new test utilities for loading test image data and managing layout messages.
- Added snapshot tests for image and layers panes to ensure consistent rendering across different states.
This commit is contained in:
Aslan Dukaev 2026-01-16 12:25:43 +03:00
commit 83a648b3ef
14 changed files with 1564 additions and 19 deletions

134
AGENTS.md Normal file
View file

@ -0,0 +1,134 @@
# AGENTS.md
Инструкции для AI агентов, работающих с этим репозиторием.
## 🔥 КРИТИЧЕСКИ ВАЖНО: Snapshot тесты для UI
### Почему это важно
В этом проекте используется **snapshot тестирование** для UI компонентов (TUI). Это критически важно для предотвращения случайных изменений в визуальной части приложения.
**ВАЖНО:** Любые изменения в коде UI могут сломать верстку. Snapshot тесты гарантируют, что дизайн останется стабильным.
### Что делать ПЕРЕД внесением изменений в UI
**НЕОБХОДИМО** запустить snapshot тесты:
```bash
# Запустить тесты файлового дерева (ОБЯЗАТЕЛЬНО)
go test -v ./cmd/dive/cli/internal/ui/v2/panes/filetree
# Или все UI тесты сразу
go test -v ./cmd/dive/cli/internal/ui/v2/panes/...
```
### Что делать ПОСЛЕ внесения изменений в UI
Если вы изменили что-то в UI (стили, верстку, отступы и т.д.):
1. **ОБЯЗАТЕЛЬНО** обновите snapshot файлы:
```bash
task unit-update-snapshots
```
2. **ПРОВЕРЬТЕ** что изменения визуально корректны:
```bash
go test -v ./cmd/dive/cli/internal/ui/v2/panes/...
```
3. **УБЕДИТЕЛЬНО** что все тесты проходят
### Правила работы с UI кодом
#### ✅ ДОПУСТИМЫЕ ИЗМЕНЕНИЯ:
- Изменение логики работы компонента (если визуально ничего не меняется)
- Оптимизация производительности
- Рефакторинг (если результат визуально идентичен)
- Добавление новых фич (с обновлением snapshot)
#### ❌ ЗАПРЕЩЕНО:
- Вносить изменения в UI БЕЗ запуска тестов
- Игнорировать падающие snapshot тесты
- Обновлять snapshot файлы "на всякий случай" (только если есть реальные изменения)
### Что делать если тесты падают
1. **Посмотрите на diff** - тест покажет что именно изменилось
2. **Если изменениеEXPECTED** (вы намеренно меняли дизайн):
- Обновите snapshot: `task unit-update-snapshots`
3. **Если изменение НЕОЖИДАНО** (случайно сломали верстку):
- Исправьте код
- НЕ обновляйте snapshot файлы
### Структура snapshot тестов
```
cmd/dive/cli/internal/ui/v2/panes/
├── filetree/
│ ├── pane.go
│ ├── pane_test.go # Тесты
│ └── __snapshots__/
│ └── pane_test.snap # Golden файлы (эталоны)
├── layers/
│ ├── pane.go
│ ├── pane_test.go
│ └── __snapshots__/
│ └── pane_test.snap
├── details/
│ ├── pane.go
│ ├── pane_test.go
│ └── __snapshots__/
│ └── pane_test.snap
└── image/
├── pane.go
├── pane_test.go
└── __snapshots__/
└── pane_test.snap
```
### Дополнительные ресурсы
- Документация по snapshot тестам: `cmd/dive/cli/internal/ui/v2/panes/README.md`
- [go-snaps documentation](https://github.com/gkampitakis/go-snaps)
---
## Другие важные инструкции
### Запуск всех тестов
```bash
# Все unit тесты
task unit
# Все тесты (unit + CLI)
task test
```
### Проверка качества кода
```bash
# Форматирование
task format
# Линтинг
task lint
# Все проверки
task pr-validations
```
---
## 🚨 CHECKLIST для агента
Перед тем как сказать "я закончил", агент ДОЛЖЕН:
- [ ] Запустил `go test -v ./cmd/dive/cli/internal/ui/v2/panes/filetree`
- [ ] Если тесты падают → понял почему (случайное изменение или реальное)
- [ ] Если вносил изменения в UI → обновил snapshot файлы
- [ ] Все тесты проходят SUCCESSFULLY
**НЕ ЗАБУДЬТЕ ПРО СNAPSHOT ТЕСТЫ! Это критически важно для стабильности UI!** 🎨

View file

@ -182,6 +182,16 @@ tasks:
- cmd: ".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt"
silent: true
unit-update-snapshots:
desc: Update snapshot test golden files
deps:
- tmpdir
vars:
TEST_PKGS:
sh: "go list ./... | grep -v '^github.com/wagoodman/dive/cmd/dive/cli$' | tr '\n' ' '"
cmds:
- "go test {{ .TEST_PKGS }} -update -v"
cli:
desc: Run CLI tests
cmds:

View file

@ -0,0 +1,118 @@
# UI v2 Snapshot Testing
Этот каталог содержит snapshot тесты для UI компонентов v2. Snapshot тестирование гарантирует визуальную стабильность TUI компонентов.
## Зачем это нужно?
В TUI приложениях самая частая проблема — "я поправил отступ здесь, а развалилась верстка там". Snapshot тесты решают эту проблему:
1. **Гарантия визуальной стабильности**: Любое случайное изменение в отступах или верстке будет немедленно обнаружено
2. **Документация**: Golden-файлы служат примерами того, как должен выглядеть компонент в разных состояниях
3. **Безопасный рефакторинг**: Можно смело менять стили, зная что тесты покажут диффер
## Структура
```
panes/
├── filetree/
│ ├── pane.go
│ ├── pane_test.go # Snapshot тесты
│ └── __snapshots__/
│ └── pane_test.snap # Golden файлы
├── layers/
│ ├── pane.go
│ ├── pane_test.go
│ └── __snapshots__/
│ └── pane_test.snap
├── details/
│ ├── pane.go
│ ├── pane_test.go
│ └── __snapshots__/
│ └── pane_test.snap
└── image/
├── pane.go
├── pane_test.go
└── __snapshots__/
└── pane_test.snap
```
## Запуск тестов
### Запустить все snapshot тесты:
```bash
task unit
```
### Обновить golden файлы (после изменения UI):
```bash
task unit-update-snapshots
```
### Запустить тесты для конкретной панели:
```bash
go test -v ./cmd/dive/cli/internal/ui/v2/panes/filetree
```
### Обновить snapshot для конкретной панели:
```bash
go test -v ./cmd/dive/cli/internal/ui/v2/panes/filetree -update
```
## Написание snapshot тестов
Пример теста:
```go
func TestPane_View_Focused(t *testing.T) {
// 1. Подготовка тестовых данных
testData := testutils.LoadTestImage(t)
// 2. Создание компонента
pane := New(testData.TreeVM)
pane.SetSize(50, 20)
// 3. Изменение состояния
pane.Update(FocusStateMsg{Focused: true})
// 4. Сравнение с эталоном
view := pane.View()
snaps.MatchSnapshot(t, view)
}
```
## Лучшие практики
1. **Тестируйте разные состояния**: Unfocused, Focused, Different Sizes, Different Data
2. **Используйте описательные имена тестов**: `TestPane_View_Focused`, `TestPane_View_SmallWidth`
3. **Не тестируйте каждую комбинацию**: Фокусируйтесь на важных edge cases
4. **Обновляйте snapshot осознанно**: Каждый раз при обновлении проверяйте diff
## Когда обновлять golden файлы?
Обновляйте snapshot когда:
- ✅ Вы намеренно меняете стиль или верстку
- ✅ Вы добавляете новый фичу в UI
- ✅ Вы рефакторите и результат визуально идентичен
НЕ обновляйте когда:
- ❌ Вы случайно сломали верстку
- ❌ Вы не понимаете почему изменился вывод
- ❌ Тест падает на CI (это значит есть проблема)
## Troubleshooting
### Тест падает с "Snapshot not found"
Это нормально при первом запуске. Запустите с `-update` чтобы создать snapshot.
### Тест падает с "Snapshot mismatch"
1. Посмотрите на diff в выводе теста
2. Если изменение ожидаемое → запустите с `-update`
3. Если нет → исправьте код
### Проблемы с путями к файлам
Убедитесь что вы запускаете тесты из корня репозитория.
## Дополнительные ресурсы
- [go-snaps documentation](https://github.com/gkampitakis/go-snaps)
- [Bubbletea testing best practices](https://github.com/charmbracelet/bubbletea/tree/master/tutorials)

View file

@ -0,0 +1,122 @@
[TestPane_View_NoLayer - 1]
╭────────────────────────────────────────────────╮
│Layer Details │
│ │
│No details │
│ │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────╯
---
[TestPane_View_WithLayer - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layer Details │
│ │
│Tags: (unavailable) │
│Id: 28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694 │
│Size: 1.1 MB │
│Digest: sha256:23bc2b70b2014dec0ac22f27bb93e9babd08cdd6f1115d0c955b9ff22b38...│
│Command: │
│#(nop) ADD │
│file:ce026b62356eec3ad1214f92be2c9dc063fe205bd5e600be3492c4dfb17148bd in / │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_Focused - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layer Details │
│ │
│Tags: (unavailable) │
│Id: 28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694 │
│Size: 1.1 MB │
│Digest: sha256:23bc2b70b2014dec0ac22f27bb93e9babd08cdd6f1115d0c955b9ff22b38...│
│Command: │
│#(nop) ADD │
│file:ce026b62356eec3ad1214f92be2c9dc063fe205bd5e600be3492c4dfb17148bd in / │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_SmallWidth - 1]
╭────────────────────────────╮
│Layer Details │
│ │
│Tags: (unavailable) │
│Id: │
│28cfe03618aa2e914e81fdd90345│
│245c15f4478e35252c06ca52d238│
│fd3cc694 │
│Size: 1.1 MB │
│Digest: sha256:23bc2b70b2...│
│Command: │
│#(nop) ADD │
│...e3492c4dfb17148bd in... │
│ │
╰────────────────────────────╯
---
[TestPane_View_SmallHeight - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layer Details │
│ │
│Tags: (unavailable) │
│Id: 28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694 │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_LargeSize - 1]
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Layer Details │
│ │
│Tags: (unavailable) │
│Id: 28cfe03618aa2e914e81fdd90345245c15f4478e35252c06ca52d238fd3cc694 │
│Size: 1.1 MB │
│Digest: sha256:23bc2b70b2014dec0ac22f27bb93e9babd08cdd6f1115d0c955b9ff22b382f5a │
│Command: │
│#(nop) ADD file:ce026b62356eec3ad1214f92be2c9dc063fe205bd5e600be3492c4dfb17148bd in / │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_Update_WithLayoutMsg - 1]
╭────────────────────────────────────────────────╮
│Layer Details │
│ │
│Tags: (unavailable) │
│Id: │
│28cfe03618aa2e914e81fdd90345245c15f4478e35252c06│
│ca52d238fd3cc694 │
│Size: 1.1 MB │
│Digest: sha256:23bc2b70b2014dec0ac22f27bb93e9...│
╰────────────────────────────────────────────────╯
---

View file

@ -0,0 +1,164 @@
package details
import (
"testing"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/testutils"
"github.com/wagoodman/dive/dive/image"
)
func TestPane_View_NoLayer(t *testing.T) {
pane := New()
pane.SetSize(50, 10)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_WithLayer(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Get first layer
if len(testData.Analysis.Layers) == 0 {
t.Skip("No layers in test data")
}
layer := testData.Analysis.Layers[0]
pane := New()
pane.SetLayer(layer)
pane.SetSize(80, 15)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_Focused(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Get first layer
if len(testData.Analysis.Layers) == 0 {
t.Skip("No layers in test data")
}
layer := testData.Analysis.Layers[0]
pane := New()
pane.SetLayer(layer)
pane.SetSize(80, 15)
// Send focus message
updatedPane, _ := pane.Update(FocusStateMsg{Focused: true})
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallWidth(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Get first layer
if len(testData.Analysis.Layers) == 0 {
t.Skip("No layers in test data")
}
layer := testData.Analysis.Layers[0]
pane := New()
pane.SetLayer(layer)
pane.SetSize(30, 15) // Very narrow width
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallHeight(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Get first layer
if len(testData.Analysis.Layers) == 0 {
t.Skip("No layers in test data")
}
layer := testData.Analysis.Layers[0]
pane := New()
pane.SetLayer(layer)
pane.SetSize(80, 6) // Very short height
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_LargeSize(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Get first layer
if len(testData.Analysis.Layers) == 0 {
t.Skip("No layers in test data")
}
layer := testData.Analysis.Layers[0]
pane := New()
pane.SetLayer(layer)
pane.SetSize(120, 30) // Large dimensions
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_LongCommand(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Find a layer with a long command
var targetLayer *image.Layer
for _, layer := range testData.Analysis.Layers {
if len(layer.Command) > 100 {
targetLayer = layer
break
}
}
if targetLayer == nil {
t.Skip("No layer with long command found")
}
pane := New()
pane.SetLayer(targetLayer)
pane.SetSize(80, 15)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_Update_WithLayoutMsg(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Get first layer
if len(testData.Analysis.Layers) == 0 {
t.Skip("No layers in test data")
}
layer := testData.Analysis.Layers[0]
pane := New()
pane.SetLayer(layer)
// Send layout message
layoutMsg := testutils.TestLayout()
updatedPane, _ := pane.Update(layoutMsg)
// Verify size was updated
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}

View file

@ -0,0 +1,203 @@
[TestPane_View_EmptyTree - 1]
╭────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│No items. │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────╯
---
[TestPane_View_WithTree - 1]
╭────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│├─󰝰 bin - drwxr-xr-x│
││ ├─󰈔 [ 1.0 MB - -rwxr-xr-x│
││ ├─󰈔 [[ 0 B - -rwxr-xr-x│
││ ├─󰈔 acpid 0 B - -rwxr-xr-x│
││ ├─󰈔 add-shell 0 B - -rwxr-xr-x│
││ ├─󰈔 addgroup 0 B - -rwxr-xr-x│
││ ├─󰈔 adduser 0 B - -rwxr-xr-x│
││ ├─󰈔 adjtimex 0 B - -rwxr-xr-x│
││ ├─󰈔 ar 0 B - -rwxr-xr-x│
││ ├─󰈔 arch 0 B - -rwxr-xr-x│
││ ├─󰈔 arp 0 B - -rwxr-xr-x│
││ ├─󰈔 arping 0 B - -rwxr-xr-x│
││ ├─󰈔 ash 0 B - -rwxr-xr-x│
││ ├─󰈔 awk 0 B - -rwxr-xr-x│
││ ├─󰈔 base64 0 B - -rwxr-xr-x│
╰────────────────────────────────────────────────╯
---
[TestPane_View_Focused - 1]
╭────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│├─󰝰 bin - drwxr-xr-x│
││ ├─󰈔 [ 1.0 MB - -rwxr-xr-x│
││ ├─󰈔 [[ 0 B - -rwxr-xr-x│
││ ├─󰈔 acpid 0 B - -rwxr-xr-x│
││ ├─󰈔 add-shell 0 B - -rwxr-xr-x│
││ ├─󰈔 addgroup 0 B - -rwxr-xr-x│
││ ├─󰈔 adduser 0 B - -rwxr-xr-x│
││ ├─󰈔 adjtimex 0 B - -rwxr-xr-x│
││ ├─󰈔 ar 0 B - -rwxr-xr-x│
││ ├─󰈔 arch 0 B - -rwxr-xr-x│
││ ├─󰈔 arp 0 B - -rwxr-xr-x│
││ ├─󰈔 arping 0 B - -rwxr-xr-x│
││ ├─󰈔 ash 0 B - -rwxr-xr-x│
││ ├─󰈔 awk 0 B - -rwxr-xr-x│
││ ├─󰈔 base64 0 B - -rwxr-xr-x│
╰────────────────────────────────────────────────╯
---
[TestPane_View_SmallWidth - 1]
╭──────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID │
│Permissions │
│├─󰝰 bin - │
│drwxr-xr-x │
││ ├─󰈔 [ 1.0 MB - │
│-rwxr-xr-x │
││ ├─󰈔 [[ 0 B - │
│-rwxr-xr-x │
││ ├─󰈔 acpid 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 add-shell 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 addgroup 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 adduser 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 adjtimex 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 ar 0 B - │
│-rwxr-xr-x │
││ ├─󰈔 arch 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 arp 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 arping 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 ash 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 awk 0 B │
│- -rwxr-xr-x │
││ ├─󰈔 base64 0 B │
│- -rwxr-xr-x │
╰──────────────────────────────────╯
---
[TestPane_View_SmallHeight - 1]
╭────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│├─󰝰 bin - drwxr-xr-x│
││ ├─󰈔 [ 1.0 MB - -rwxr-xr-x│
││ ├─󰈔 [[ 0 B - -rwxr-xr-x│
╰────────────────────────────────────────────────╯
---
[TestPane_View_LargeSize - 1]
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│├─󰝰 bin - drwxr-xr-x│
││ ├─󰈔 [ 1.0 MB - -rwxr-xr-x│
││ ├─󰈔 [[ 0 B - -rwxr-xr-x│
││ ├─󰈔 acpid 0 B - -rwxr-xr-x│
││ ├─󰈔 add-shell 0 B - -rwxr-xr-x│
││ ├─󰈔 addgroup 0 B - -rwxr-xr-x│
││ ├─󰈔 adduser 0 B - -rwxr-xr-x│
││ ├─󰈔 adjtimex 0 B - -rwxr-xr-x│
││ ├─󰈔 ar 0 B - -rwxr-xr-x│
││ ├─󰈔 arch 0 B - -rwxr-xr-x│
││ ├─󰈔 arp 0 B - -rwxr-xr-x│
││ ├─󰈔 arping 0 B - -rwxr-xr-x│
││ ├─󰈔 ash 0 B - -rwxr-xr-x│
││ ├─󰈔 awk 0 B - -rwxr-xr-x│
││ ├─󰈔 base64 0 B - -rwxr-xr-x│
││ ├─󰈔 basename 0 B - -rwxr-xr-x│
││ ├─󰈔 beep 0 B - -rwxr-xr-x│
││ ├─󰈔 blkdiscard 0 B - -rwxr-xr-x│
││ ├─󰈔 blkid 0 B - -rwxr-xr-x│
││ ├─󰈔 blockdev 0 B - -rwxr-xr-x│
││ ├─󰈔 bootchartd 0 B - -rwxr-xr-x│
││ ├─󰈔 brctl 0 B - -rwxr-xr-x│
││ ├─󰈔 bunzip2 0 B - -rwxr-xr-x│
││ ├─󰈔 busybox 0 B - -rwxr-xr-x│
││ ├─󰈔 bzcat 0 B - -rwxr-xr-x│
││ ├─󰈔 bzip2 0 B - -rwxr-xr-x│
││ ├─󰈔 cal 0 B - -rwxr-xr-x│
││ ├─󰈔 cat 0 B - -rwxr-xr-x│
││ ├─󰈔 chat 0 B - -rwxr-xr-x│
││ ├─󰈔 chattr 0 B - -rwxr-xr-x│
││ ├─󰈔 chgrp 0 B - -rwxr-xr-x│
││ ├─󰈔 chmod 0 B - -rwxr-xr-x│
││ ├─󰈔 chown 0 B - -rwxr-xr-x│
││ ├─󰈔 chpasswd 0 B - -rwxr-xr-x│
││ ├─󰈔 chpst 0 B - -rwxr-xr-x│
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_Update_WithLayoutMsg - 1]
╭────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│├─󰝰 bin - drwxr-xr-x│
││ ├─󰈔 [ 1.0 MB - -rwxr-xr-x│
││ ├─󰈔 [[ 0 B - -rwxr-xr-x│
││ ├─󰈔 acpid 0 B - -rwxr-xr-x│
││ ├─󰈔 add-shell 0 B - -rwxr-xr-x│
││ ├─󰈔 addgroup 0 B - -rwxr-xr-x│
││ ├─󰈔 adduser 0 B - -rwxr-xr-x│
││ ├─󰈔 adjtimex 0 B - -rwxr-xr-x│
││ ├─󰈔 ar 0 B - -rwxr-xr-x│
││ ├─󰈔 arch 0 B - -rwxr-xr-x│
╰────────────────────────────────────────────────╯
---
[TestPane_Update_TreeNavigation - 1]
╭────────────────────────────────────────────────╮
│Current Layer Contents │
│ │
│Name Size UID:GID Permissions│
│├─󰝰 bin - drwxr-xr-x│
││ ├─󰈔 [ 1.0 MB - -rwxr-xr-x│
││ ├─󰈔 [[ 0 B - -rwxr-xr-x│
││ ├─󰈔 acpid 0 B - -rwxr-xr-x│
││ ├─󰈔 add-shell 0 B - -rwxr-xr-x│
││ ├─󰈔 addgroup 0 B - -rwxr-xr-x│
││ ├─󰈔 adduser 0 B - -rwxr-xr-x│
││ ├─󰈔 adjtimex 0 B - -rwxr-xr-x│
││ ├─󰈔 ar 0 B - -rwxr-xr-x│
││ ├─󰈔 arch 0 B - -rwxr-xr-x│
││ ├─󰈔 arp 0 B - -rwxr-xr-x│
││ ├─󰈔 arping 0 B - -rwxr-xr-x│
││ ├─󰈔 ash 0 B - -rwxr-xr-x│
││ ├─󰈔 awk 0 B - -rwxr-xr-x│
││ ├─󰈔 base64 0 B - -rwxr-xr-x│
╰────────────────────────────────────────────────╯
---

View file

@ -128,31 +128,33 @@ func (p Pane) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case common.LocalMouseMsg:
// Handle mouse events manually since bubbles/list doesn't understand LocalMouseMsg
if msg.Action == tea.MouseActionPress {
// Content offsets relative to the panel:
// Y: 1 (top border) + 1 (box title) + 1 (empty line) + 1 (table header) = 4
// X: 1 (left border)
const contentOffsetY = 4
const contentOffsetX = 1
switch msg.Button {
case tea.MouseButtonWheelUp, tea.MouseButtonWheelDown:
// IMPORTANT: For scrolling, pass the ORIGINAL message (msg.MouseMsg)
// directly to the list component. bubbles/list knows how to properly
// scroll the viewport when it receives standard wheel events.
// Using CursorUp/Down here was incorrect as they only move the cursor,
// not the viewport.
// CRITICAL FIX:
// bubbles/list has Hit Test: if Y < 0 or Y > Height, event is ignored.
// We must pass coordinates RELATIVE TO THE LIST ITSELF (accounting for offsets),
// otherwise scroll only works at the top of the list.
localMsg := msg.MouseMsg
localMsg.X = msg.LocalX - contentOffsetX
localMsg.Y = msg.LocalY - contentOffsetY
var cmd tea.Cmd
p.list, cmd = p.list.Update(msg.MouseMsg)
p.list, cmd = p.list.Update(localMsg)
return p, cmd
case tea.MouseButtonLeft:
// Calculate item index from Y coordinate
// Content offset consists of:
// 1 (top border) + 1 (box title) + 1 (empty line) + 1 (table header) = 4
const contentOffsetY = 4
// Local Y coordinate within the list
// For clicks, use the same Y offset logic
clickY := msg.LocalY - contentOffsetY
// Ignore clicks above/below the list content
// Ignore clicks on headers (negative coordinates relative to list)
if clickY >= 0 {
// Calculate absolute index of the item in the list
// Start() returns the index of the first visible item
// (p.list.Index() - p.list.Cursor()) is the index of the top item
// Calculate absolute item index
firstVisibleIndex := p.list.Index() - p.list.Cursor()
targetIndex := firstVisibleIndex + clickY
@ -160,7 +162,6 @@ func (p Pane) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if targetIndex >= 0 && targetIndex < len(p.list.Items()) {
p.list.Select(targetIndex)
// Click selects file and toggles folder
// Send selection changed message and toggle command
return p, tea.Batch(
func() tea.Msg { return TreeSelectionChangedMsg{NodeIndex: targetIndex} },
p.toggleCollapse(),

View file

@ -0,0 +1,115 @@
package filetree
import (
"testing"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/stretchr/testify/require"
tea "github.com/charmbracelet/bubbletea"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/testutils"
)
func TestPane_View_EmptyTree(t *testing.T) {
// Test with nil treeVM
pane := New(nil)
pane.SetSize(50, 20)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_WithTree(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
pane.SetSize(50, 20)
// Initialize the pane
cmd := pane.Init()
require.Nil(t, cmd)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_Focused(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
pane.SetSize(50, 20)
// Send focus message
updatedPane, _ := pane.Update(FocusStateMsg{Focused: true})
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallWidth(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
pane.SetSize(30, 20) // Very narrow width
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallHeight(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
pane.SetSize(50, 8) // Very short height
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_LargeSize(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
pane.SetSize(120, 40) // Large dimensions
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_Update_WithLayoutMsg(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
// Send layout message
layoutMsg := testutils.TestLayout()
updatedPane, _ := pane.Update(layoutMsg)
// Verify size was updated
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}
func TestPane_Update_TreeNavigation(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.TreeVM)
pane.SetSize(50, 20)
// Focus the pane
pane.Update(FocusStateMsg{Focused: true})
// Move down a few items
pane.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
pane.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
view := pane.View()
snaps.MatchSnapshot(t, view)
}

View file

@ -34,7 +34,7 @@ func CollectVisibleNodes(root *filetree.FileNode) []VisibleNode {
if i == len(levels)-1 {
// Current level (the node itself) - 2 chars
if isLast {
prefixBuilder.WriteString("─") // Was "└── "
prefixBuilder.WriteString("─") // Was "└── "
} else {
prefixBuilder.WriteString("├─") // Was "├── "
}
@ -77,7 +77,7 @@ func CollectVisibleNodes(root *filetree.FileNode) []VisibleNode {
sortedChildren := SortChildren(root.Children)
count := len(sortedChildren)
for i, child := range sortedChildren {
traverse(child, []bool{i == count - 1})
traverse(child, []bool{i == count-1})
}
}

View file

@ -0,0 +1,161 @@
[TestPane_View_NoAnalysis - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Image Details │
│ │
│No image data │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_WithAnalysis - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Image Details │
│ │
│Image name: /Users/aslan/Documents/oss/dive/.data/test-docker-image.tar │
│Total Image size: 1.2 MB │
│Potential wasted space: 31.3 KB │
│Image efficiency score: 98% │
│Files > 0 KB total: 0 │
│ │
│Count Total Space Path │
│2 6.3 KB /root/example/somefile3.txt │
│2 12.5 KB /root/example/somefile1.txt │
│2 12.5 KB /root/saved.txt │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_Focused - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Image Details │
│ │
│Image name: /Users/aslan/Documents/oss/dive/.data/test-docker-image.tar │
│Total Image size: 1.2 MB │
│Potential wasted space: 31.3 KB │
│Image efficiency score: 98% │
│Files > 0 KB total: 0 │
│ │
│Count Total Space Path │
│2 6.3 KB /root/example/somefile3.txt │
│2 12.5 KB /root/example/somefile1.txt │
│2 12.5 KB /root/saved.txt │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_SmallWidth - 1]
╭──────────────────────────────────────╮
│Image Details │
│ │
│Image name: /Users/aslan/Documents/oss│
│Total Image size: 1.2 MB │
│Potential wasted space: 31.3 KB │
│Image efficiency score: 98% │
│Files > 0 KB total: 0 │
│ │
│Count Total Space Path │
│2 6.3 KB /root/example/so...│
│2 12.5 KB /root/example/so...│
│2 12.5 KB /root/saved.txt │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────╯
---
[TestPane_View_SmallHeight - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Image Details │
│ │
│Image name: /Users/aslan/Documents/oss/dive/.data/test-docker-image.tar │
│Total Image size: 1.2 MB │
│Potential wasted space: 31.3 KB │
│Image efficiency score: 98% │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_LargeSize - 1]
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Image Details │
│ │
│Image name: /Users/aslan/Documents/oss/dive/.data/test-docker-image.tar │
│Total Image size: 1.2 MB │
│Potential wasted space: 31.3 KB │
│Image efficiency score: 98% │
│Files > 0 KB total: 0 │
│ │
│Count Total Space Path │
│2 6.3 KB /root/example/somefile3.txt │
│2 12.5 KB /root/example/somefile1.txt │
│2 12.5 KB /root/saved.txt │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_Update_WithLayoutMsg - 1]
╭────────────────────────────────────────────────╮
│Image Details │
│ │
│Image name: /Users/aslan/Documents/oss/dive/.dat│
│Total Image size: 1.2 MB │
│Potential wasted space: 31.3 KB │
│Image efficiency score: 98% │
│Files > 0 KB total: 0 │
│ │
│Count Total Space Path │
│2 6.3 KB /root/example/somefile3.txt │
╰────────────────────────────────────────────────╯
---

View file

@ -0,0 +1,96 @@
package image
import (
"testing"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/stretchr/testify/require"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/testutils"
)
func TestPane_View_NoAnalysis(t *testing.T) {
// Test with nil analysis
pane := New(nil)
pane.SetSize(80, 20)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_WithAnalysis(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.Analysis)
pane.SetSize(80, 20)
// Initialize the pane
cmd := pane.Init()
require.Nil(t, cmd)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_Focused(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.Analysis)
pane.SetSize(80, 20)
// Send focus message
updatedPane, _ := pane.Update(FocusStateMsg{Focused: true})
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallWidth(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.Analysis)
pane.SetSize(40, 20) // Narrow width
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallHeight(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.Analysis)
pane.SetSize(80, 8) // Short height
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_LargeSize(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.Analysis)
pane.SetSize(120, 40) // Large dimensions
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_Update_WithLayoutMsg(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
pane := New(testData.Analysis)
// Send layout message
layoutMsg := testutils.TestLayout()
updatedPane, _ := pane.Update(layoutMsg)
// Verify size was updated
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}

View file

@ -0,0 +1,179 @@
[TestPane_View_EmptyState - 1]
╭────────────────────────────────────────────────╮
│Layers │
│ │
│No layer data │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰────────────────────────────────────────────────╯
---
[TestPane_View_WithLayers - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(nop) ADD f... 23bc2b70b201 │
│[2/14] 1871059774ab 6.3 KB +1 0 0 #(nop) ADD f... a65b7d7ac139 │
│[3/14] 49fe2a475548 0 B +1 0 0 mkdir -p /ro... 93e208d47175 │
│[4/14] 80cd2ca1ffc8 6.3 KB +1 0 0 cp /somefile... 4abad3abe3cb │
│[5/14] c99e2f8d3f62 6.3 KB 0 ~1 0 chmod 444 /r... 14c9a6ffcb6a │
│[6/14] 5eca617bdc3b 6.3 KB +1 0 0 cp /somefile... 778fb5770ef4 │
│[7/14] f07c3eb88757 6.3 KB +1 0 0 cp /somefile... f275b8a31a71 │
│[8/14] 461885fc2258 6.3 KB +1 0 -1 mv /root/exa... dd1effc5eb19 │
│[9/14] a10327f68ffe 6.3 KB +1 0 0 cp /root/sav... 8d1869a0a066 │
│[10/14] f2fc54e25cb7 0 B 0 0 -3 rm -rf /root... bc2e36423fa3 │
│[11/14] aad36d0b05e7 2.1 KB +2 0 0 #(nop) ADD d... 7f648d45ee7b │
│[12/14] 3d4ad907517a 6.3 KB +1 0 0 cp /root/sav... a4b8f95f266d │
│[13/14] 81b1b002d4b4 6.3 KB +1 0 0 cp /root/sav... 22a44d45780a │
│[14/14] cfb35bb5c127 6.3 KB 0 ~1 0 chmod +x /ro... ba689cac6a98 │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_Focused - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(nop) ADD f... 23bc2b70b201 │
│[2/14] 1871059774ab 6.3 KB +1 0 0 #(nop) ADD f... a65b7d7ac139 │
│[3/14] 49fe2a475548 0 B +1 0 0 mkdir -p /ro... 93e208d47175 │
│[4/14] 80cd2ca1ffc8 6.3 KB +1 0 0 cp /somefile... 4abad3abe3cb │
│[5/14] c99e2f8d3f62 6.3 KB 0 ~1 0 chmod 444 /r... 14c9a6ffcb6a │
│[6/14] 5eca617bdc3b 6.3 KB +1 0 0 cp /somefile... 778fb5770ef4 │
│[7/14] f07c3eb88757 6.3 KB +1 0 0 cp /somefile... f275b8a31a71 │
│[8/14] 461885fc2258 6.3 KB +1 0 -1 mv /root/exa... dd1effc5eb19 │
│[9/14] a10327f68ffe 6.3 KB +1 0 0 cp /root/sav... 8d1869a0a066 │
│[10/14] f2fc54e25cb7 0 B 0 0 -3 rm -rf /root... bc2e36423fa3 │
│[11/14] aad36d0b05e7 2.1 KB +2 0 0 #(nop) ADD d... 7f648d45ee7b │
│[12/14] 3d4ad907517a 6.3 KB +1 0 0 cp /root/sav... a4b8f95f266d │
│[13/14] 81b1b002d4b4 6.3 KB +1 0 0 cp /root/sav... 22a44d45780a │
│[14/14] cfb35bb5c127 6.3 KB 0 ~1 0 chmod +x /ro... ba689cac6a98 │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_SmallWidth - 1]
╭──────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(│
│[2/14] 1871059774ab 6.3 KB +1 0 0 #│
│[3/14] 49fe2a475548 0 B +1 0 0 m│
│[4/14] 80cd2ca1ffc8 6.3 KB +1 0 0 c│
│[5/14] c99e2f8d3f62 6.3 KB 0 ~1 0 c│
│[6/14] 5eca617bdc3b 6.3 KB +1 0 0 c│
│[7/14] f07c3eb88757 6.3 KB +1 0 0 c│
│[8/14] 461885fc2258 6.3 KB +1 0 -1 │
│[9/14] a10327f68ffe 6.3 KB +1 0 0 c│
│[10/14] f2fc54e25cb7 0 B 0 0 -3 │
│[11/14] aad36d0b05e7 2.1 KB +2 0 0 │
│[12/14] 3d4ad907517a 6.3 KB +1 0 0 │
│[13/14] 81b1b002d4b4 6.3 KB +1 0 0 │
│[14/14] cfb35bb5c127 6.3 KB 0 ~1 0 │
│ │
│ │
╰──────────────────────────────────────╯
---
[TestPane_View_SmallHeight - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(nop) ADD f... 23bc2b70b201 │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_LargeSize - 1]
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(nop) ADD f... 23bc2b70b201 │
│[2/14] 1871059774ab 6.3 KB +1 0 0 #(nop) ADD f... a65b7d7ac139 │
│[3/14] 49fe2a475548 0 B +1 0 0 mkdir -p /ro... 93e208d47175 │
│[4/14] 80cd2ca1ffc8 6.3 KB +1 0 0 cp /somefile... 4abad3abe3cb │
│[5/14] c99e2f8d3f62 6.3 KB 0 ~1 0 chmod 444 /r... 14c9a6ffcb6a │
│[6/14] 5eca617bdc3b 6.3 KB +1 0 0 cp /somefile... 778fb5770ef4 │
│[7/14] f07c3eb88757 6.3 KB +1 0 0 cp /somefile... f275b8a31a71 │
│[8/14] 461885fc2258 6.3 KB +1 0 -1 mv /root/exa... dd1effc5eb19 │
│[9/14] a10327f68ffe 6.3 KB +1 0 0 cp /root/sav... 8d1869a0a066 │
│[10/14] f2fc54e25cb7 0 B 0 0 -3 rm -rf /root... bc2e36423fa3 │
│[11/14] aad36d0b05e7 2.1 KB +2 0 0 #(nop) ADD d... 7f648d45ee7b │
│[12/14] 3d4ad907517a 6.3 KB +1 0 0 cp /root/sav... a4b8f95f266d │
│[13/14] 81b1b002d4b4 6.3 KB +1 0 0 cp /root/sav... 22a44d45780a │
│[14/14] cfb35bb5c127 6.3 KB 0 ~1 0 chmod +x /ro... ba689cac6a98 │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_View_SecondLayerSelected - 1]
╭──────────────────────────────────────────────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(nop) ADD f... 23bc2b70b201 │
│[2/14] 1871059774ab 6.3 KB +1 0 0 #(nop) ADD f... a65b7d7ac139 │
│[3/14] 49fe2a475548 0 B +1 0 0 mkdir -p /ro... 93e208d47175 │
│[4/14] 80cd2ca1ffc8 6.3 KB +1 0 0 cp /somefile... 4abad3abe3cb │
│[5/14] c99e2f8d3f62 6.3 KB 0 ~1 0 chmod 444 /r... 14c9a6ffcb6a │
│[6/14] 5eca617bdc3b 6.3 KB +1 0 0 cp /somefile... 778fb5770ef4 │
│[7/14] f07c3eb88757 6.3 KB +1 0 0 cp /somefile... f275b8a31a71 │
│[8/14] 461885fc2258 6.3 KB +1 0 -1 mv /root/exa... dd1effc5eb19 │
│[9/14] a10327f68ffe 6.3 KB +1 0 0 cp /root/sav... 8d1869a0a066 │
│[10/14] f2fc54e25cb7 0 B 0 0 -3 rm -rf /root... bc2e36423fa3 │
│[11/14] aad36d0b05e7 2.1 KB +2 0 0 #(nop) ADD d... 7f648d45ee7b │
│[12/14] 3d4ad907517a 6.3 KB +1 0 0 cp /root/sav... a4b8f95f266d │
│[13/14] 81b1b002d4b4 6.3 KB +1 0 0 cp /root/sav... 22a44d45780a │
│[14/14] cfb35bb5c127 6.3 KB 0 ~1 0 chmod +x /ro... ba689cac6a98 │
│ │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
---
[TestPane_Update_WithLayoutMsg - 1]
╭────────────────────────────────────────────────╮
│Layers │
│ │
│[1/14] 28cfe03618aa 1.1 MB 0 0 0 #(nop) ADD f│
│[2/14] 1871059774ab 6.3 KB +1 0 0 #(nop) ADD │
│[3/14] 49fe2a475548 0 B +1 0 0 mkdir -p /r│
│[4/14] 80cd2ca1ffc8 6.3 KB +1 0 0 cp /somefil│
│[5/14] c99e2f8d3f62 6.3 KB 0 ~1 0 chmod 444 /│
│[6/14] 5eca617bdc3b 6.3 KB +1 0 0 cp /somefil│
╰────────────────────────────────────────────────╯
---

View file

@ -0,0 +1,150 @@
package layers
import (
"testing"
"github.com/gkampitakis/go-snaps/snaps"
"github.com/stretchr/testify/require"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/testutils"
)
func TestPane_View_EmptyState(t *testing.T) {
// Test with nil layerVM
pane := New(nil, testutils.LoadTestImage(t).Comparer)
pane.SetSize(50, 20)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_WithLayers(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 0,
}
pane := New(layerVM, testData.Comparer)
pane.SetSize(80, 20)
// Initialize the pane
cmd := pane.Init()
require.Nil(t, cmd)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_Focused(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 0,
}
pane := New(layerVM, testData.Comparer)
pane.SetSize(80, 20)
// Send focus message
updatedPane, _ := pane.Update(FocusStateMsg{Focused: true})
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallWidth(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 0,
}
pane := New(layerVM, testData.Comparer)
pane.SetSize(40, 20) // Narrow width
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SmallHeight(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 0,
}
pane := New(layerVM, testData.Comparer)
pane.SetSize(80, 5) // Very short height
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_LargeSize(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 0,
}
pane := New(layerVM, testData.Comparer)
pane.SetSize(120, 40) // Large dimensions
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_View_SecondLayerSelected(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel with second layer selected
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 1, // Select second layer
}
pane := New(layerVM, testData.Comparer)
pane.SetSize(80, 20)
view := pane.View()
snaps.MatchSnapshot(t, view)
}
func TestPane_Update_WithLayoutMsg(t *testing.T) {
// Load test image data
testData := testutils.LoadTestImage(t)
// Create layer viewmodel
layerVM := &viewmodel.LayerSetState{
Layers: testData.Analysis.Layers,
LayerIndex: 0,
}
pane := New(layerVM, testData.Comparer)
// Send layout message
layoutMsg := testutils.TestLayout()
updatedPane, _ := pane.Update(layoutMsg)
// Verify size was updated
view := updatedPane.(Pane).View()
snaps.MatchSnapshot(t, view)
}

View file

@ -0,0 +1,92 @@
package testutils
import (
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel"
"github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/common"
"github.com/wagoodman/dive/dive/filetree"
"github.com/wagoodman/dive/dive/image"
"github.com/wagoodman/dive/dive/image/docker"
)
var repoRootCache string
// TestImageData provides test image data for snapshot tests
type TestImageData struct {
Analysis *image.Analysis
TreeVM *viewmodel.FileTreeViewModel
Comparer filetree.Comparer
}
// repoRoot returns the root directory of the git repository
func repoRoot(t testing.TB) string {
t.Helper()
if repoRootCache != "" {
return repoRootCache
}
// use git to find the root of the repo
out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
if err != nil {
t.Fatalf("failed to get repo root: %v", err)
}
repoRootCache = strings.TrimSpace(string(out))
return repoRootCache
}
// repoPath returns a path relative to the repository root
func repoPath(t testing.TB, path string) string {
t.Helper()
root := repoRoot(t)
return root + "/" + strings.TrimPrefix(path, "./")
}
// LoadTestImage loads test image data from the test archive
func LoadTestImage(t *testing.T) TestImageData {
t.Helper()
// Load test image - use repoPath to get absolute path
result := docker.TestAnalysisFromArchive(t, repoPath(t, ".data/test-docker-image.tar"))
require.NotNil(t, result, "unable to load test data")
// Create filetree viewmodel
vm, err := viewmodel.NewFileTreeViewModel(
v1.Config{
Analysis: *result,
Preferences: v1.DefaultPreferences(),
},
0,
)
require.NoError(t, err, "unable to create viewmodel")
// Initialize ViewTree by calling Update (this sets ViewTree = ModelTree.Copy())
err = vm.Update(nil, 100, 100)
require.NoError(t, err, "unable to update viewmodel")
// Get comparer
comparer := filetree.NewComparer(result.RefTrees)
errs := comparer.BuildCache()
require.Empty(t, errs, "unable to build comparer")
return TestImageData{
Analysis: result,
TreeVM: vm,
Comparer: comparer,
}
}
// TestLayout provides a test layout message for panes
func TestLayout() common.LayoutMsg {
return common.LayoutMsg{
LeftWidth: 50,
RightWidth: 50,
LayersHeight: 10,
DetailsHeight: 8,
ImageHeight: 12,
TreeHeight: 15,
}
}