diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b225360 --- /dev/null +++ b/AGENTS.md @@ -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!** 🎨 diff --git a/Taskfile.yaml b/Taskfile.yaml index e515ed7..0f775cc 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -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: diff --git a/cmd/dive/cli/internal/ui/v2/panes/README.md b/cmd/dive/cli/internal/ui/v2/panes/README.md new file mode 100644 index 0000000..6be55ad --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/README.md @@ -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) diff --git a/cmd/dive/cli/internal/ui/v2/panes/details/__snapshots__/pane_test.snap b/cmd/dive/cli/internal/ui/v2/panes/details/__snapshots__/pane_test.snap new file mode 100755 index 0000000..014860a --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/details/__snapshots__/pane_test.snap @@ -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...│ +╰────────────────────────────────────────────────╯ +--- diff --git a/cmd/dive/cli/internal/ui/v2/panes/details/pane_test.go b/cmd/dive/cli/internal/ui/v2/panes/details/pane_test.go new file mode 100644 index 0000000..cba4948 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/details/pane_test.go @@ -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) +} diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap b/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap new file mode 100755 index 0000000..d089203 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap @@ -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│ +╰────────────────────────────────────────────────╯ +--- diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go index 31cc73b..0ecf6db 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go @@ -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(), diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/pane_test.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane_test.go new file mode 100644 index 0000000..29e6bee --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane_test.go @@ -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) +} diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/tree_traversal.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/tree_traversal.go index 96a1ee1..adedb64 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/filetree/tree_traversal.go +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/tree_traversal.go @@ -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}) } } diff --git a/cmd/dive/cli/internal/ui/v2/panes/image/__snapshots__/pane_test.snap b/cmd/dive/cli/internal/ui/v2/panes/image/__snapshots__/pane_test.snap new file mode 100755 index 0000000..925eb16 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/image/__snapshots__/pane_test.snap @@ -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 │ +╰────────────────────────────────────────────────╯ +--- diff --git a/cmd/dive/cli/internal/ui/v2/panes/image/pane_test.go b/cmd/dive/cli/internal/ui/v2/panes/image/pane_test.go new file mode 100644 index 0000000..e09eca0 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/image/pane_test.go @@ -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) +} diff --git a/cmd/dive/cli/internal/ui/v2/panes/layers/__snapshots__/pane_test.snap b/cmd/dive/cli/internal/ui/v2/panes/layers/__snapshots__/pane_test.snap new file mode 100755 index 0000000..db006b5 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/layers/__snapshots__/pane_test.snap @@ -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│ +╰────────────────────────────────────────────────╯ +--- diff --git a/cmd/dive/cli/internal/ui/v2/panes/layers/pane_test.go b/cmd/dive/cli/internal/ui/v2/panes/layers/pane_test.go new file mode 100644 index 0000000..776f722 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/panes/layers/pane_test.go @@ -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) +} diff --git a/cmd/dive/cli/internal/ui/v2/testutils/helpers.go b/cmd/dive/cli/internal/ui/v2/testutils/helpers.go new file mode 100644 index 0000000..6b5f85f --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/testutils/helpers.go @@ -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, + } +}