Navegador
feat: shell completions for bash, zsh, and fish CLI: navegador completions <shell> [--install]. Generates eval lines for tab completion, with optional install to shell rc file. Closes #60
Commit
67dab811171e9d0c621c33a839cdc17cecf13b1443a84fb8d61d6ffcadb9a881
Parent
b3b866407fcb8c4…
2 files changed
+50
+235
+50
| --- a/navegador/completions.py | ||
| +++ b/navegador/completions.py | ||
| @@ -0,0 +1,50 @@ | ||
| 1 | +""" | |
| 2 | +Shell completions — generate tab-completion scripts for bash, zsh, and fish. | |
| 3 | + | |
| 4 | +Usage: | |
| 5 | + Print the eval line to add to your shell rc file: | |
| 6 | + navegador completions bash | |
| 7 | + navegador completions zsh | |
| 8 | + navegador completions fish | |
| 9 | + | |
| 10 | + Install automatically: | |
| 11 | + navegador completions bash --install | |
| 12 | + navegador completions zsh --install | |
| 13 | + navegador completions fish --install | |
| 14 | +""" | |
| 15 | + | |
| 16 | +from __future__ import annotations | |
| 17 | + | |
| 18 | +from pathlib import Path | |
| 19 | + | |
| 20 | +SUPPORTED_SHELLS = ["bash", "zsh", "fish"] | |
| 21 | + | |
| 22 | +# The env-var source command for each shell | |
| 23 | +_SOURCE_COMMANDS: dict[str, str] = { | |
| 24 | + "bash": "_NAVEGADOR_COMPLETE=bash_source navegador", | |
| 25 | + "zsh": "_NAVEGADOR_COMPLETE=zsh_source navegador", | |
| 26 | + "fish": "_NAVEGADOR_COMPLETE=fish_source navegador", | |
| 27 | +} | |
| 28 | + | |
| 29 | +# The eval/source wrapper for each shell | |
| 30 | +_EVAL_LINES: dict[str, str] = { | |
| 31 | + "bash": 'eval "$(_NAVEGADOR_COMPLETE=bash_source navegador)"', | |
| 32 | + "zsh": 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"', | |
| 33 | + "fish": "_NAVEGADOR_COMPLETE=fish_source navegador | source", | |
| 34 | +} | |
| 35 | + | |
| 36 | +# Default rc file paths for each shell (relative to $HOME) | |
| 37 | +_RC_PATHS: dict[str, str] = { | |
| 38 | + "bash": "~/.bashrc", | |
| 39 | + "zsh": "~/.zshrc", | |
| 40 | + "fish": "~/.config/fish/config.fish", | |
| 41 | +} | |
| 42 | + | |
| 43 | + | |
| 44 | +def get_eval_line(shell: str) -> str: | |
| 45 | + """Return the eval/source line to add to the shell rc file. | |
| 46 | + | |
| 47 | + Raises ValueError for unsupported shells. | |
| 48 | + """ | |
| 49 | + if shell not in SUPPORTED_SHELLS: | |
| 50 | + raise ValueError(f"Unsupported shell: {shell!r}. Choose from: {', '.join(SUPPORTED_SHELL |
| --- a/navegador/completions.py | |
| +++ b/navegador/completions.py | |
| @@ -0,0 +1,50 @@ | |
| --- a/navegador/completions.py | |
| +++ b/navegador/completions.py | |
| @@ -0,0 +1,50 @@ | |
| 1 | """ |
| 2 | Shell completions — generate tab-completion scripts for bash, zsh, and fish. |
| 3 | |
| 4 | Usage: |
| 5 | Print the eval line to add to your shell rc file: |
| 6 | navegador completions bash |
| 7 | navegador completions zsh |
| 8 | navegador completions fish |
| 9 | |
| 10 | Install automatically: |
| 11 | navegador completions bash --install |
| 12 | navegador completions zsh --install |
| 13 | navegador completions fish --install |
| 14 | """ |
| 15 | |
| 16 | from __future__ import annotations |
| 17 | |
| 18 | from pathlib import Path |
| 19 | |
| 20 | SUPPORTED_SHELLS = ["bash", "zsh", "fish"] |
| 21 | |
| 22 | # The env-var source command for each shell |
| 23 | _SOURCE_COMMANDS: dict[str, str] = { |
| 24 | "bash": "_NAVEGADOR_COMPLETE=bash_source navegador", |
| 25 | "zsh": "_NAVEGADOR_COMPLETE=zsh_source navegador", |
| 26 | "fish": "_NAVEGADOR_COMPLETE=fish_source navegador", |
| 27 | } |
| 28 | |
| 29 | # The eval/source wrapper for each shell |
| 30 | _EVAL_LINES: dict[str, str] = { |
| 31 | "bash": 'eval "$(_NAVEGADOR_COMPLETE=bash_source navegador)"', |
| 32 | "zsh": 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"', |
| 33 | "fish": "_NAVEGADOR_COMPLETE=fish_source navegador | source", |
| 34 | } |
| 35 | |
| 36 | # Default rc file paths for each shell (relative to $HOME) |
| 37 | _RC_PATHS: dict[str, str] = { |
| 38 | "bash": "~/.bashrc", |
| 39 | "zsh": "~/.zshrc", |
| 40 | "fish": "~/.config/fish/config.fish", |
| 41 | } |
| 42 | |
| 43 | |
| 44 | def get_eval_line(shell: str) -> str: |
| 45 | """Return the eval/source line to add to the shell rc file. |
| 46 | |
| 47 | Raises ValueError for unsupported shells. |
| 48 | """ |
| 49 | if shell not in SUPPORTED_SHELLS: |
| 50 | raise ValueError(f"Unsupported shell: {shell!r}. Choose from: {', '.join(SUPPORTED_SHELL |
+235
| --- a/tests/test_completions.py | ||
| +++ b/tests/test_completions.py | ||
| @@ -0,0 +1,235 @@ | ||
| 1 | +"""Tests for shell completions — module API and CLI command.""" | |
| 2 | + | |
| 3 | +from pathlib import Path | |
| 4 | + | |
| 5 | +import pytest | |
| 6 | +from click.testing import CliRunner | |
| 7 | + | |
| 8 | +from navegador.cli.commands import main | |
| 9 | +from navegador.completions import ( | |
| 10 | + SUPPORTED_SHELLS, | |
| 11 | + get_eval_line, | |
| 12 | + get_rc_path, | |
| 13 | + install_completion, | |
| 14 | +) | |
| 15 | + | |
| 16 | +# ── get_eval_line ───────────────────────────────────────────────────────────── | |
| 17 | + | |
| 18 | + | |
| 19 | +class TestGetEvalLine: | |
| 20 | + def test_bash_contains_bash_source(self): | |
| 21 | + line = get_eval_line("bash") | |
| 22 | + assert "bash_source" in line | |
| 23 | + assert "navegador" in line | |
| 24 | + | |
| 25 | + def test_zsh_contains_zsh_source(self): | |
| 26 | + line = get_eval_line("zsh") | |
| 27 | + assert "zsh_source" in line | |
| 28 | + assert "navegador" in line | |
| 29 | + | |
| 30 | + def test_fish_contains_fish_source(self): | |
| 31 | + line = get_eval_line("fish") | |
| 32 | + assert "fish_source" in line | |
| 33 | + assert "navegador" in line | |
| 34 | + | |
| 35 | + def test_bash_uses_eval(self): | |
| 36 | + line = get_eval_line("bash") | |
| 37 | + assert line.startswith("eval ") | |
| 38 | + | |
| 39 | + def test_zsh_uses_eval(self): | |
| 40 | + line = get_eval_line("zsh") | |
| 41 | + assert line.startswith("eval ") | |
| 42 | + | |
| 43 | + def test_fish_uses_pipe_source(self): | |
| 44 | + line = get_eval_line("fish") | |
| 45 | + assert "| source" in line | |
| 46 | + | |
| 47 | + def test_invalid_shell_raises_value_error(self): | |
| 48 | + with pytest.raises(ValueError, match="Unsupported shell"): | |
| 49 | + get_eval_line("powershell") | |
| 50 | + | |
| 51 | + def test_all_supported_shells_return_string(self): | |
| 52 | + for shell in SUPPORTED_SHELLS: | |
| 53 | + assert isinstance(get_eval_line(shell), str) | |
| 54 | + | |
| 55 | + | |
| 56 | +# ── get_rc_path ─────────────────────────────────────────────────────────────── | |
| 57 | + | |
| 58 | + | |
| 59 | +class TestGetRcPath: | |
| 60 | + def test_bash_returns_bashrc(self): | |
| 61 | + assert get_rc_path("bash") == "~/.bashrc" | |
| 62 | + | |
| 63 | + def test_zsh_returns_zshrc(self): | |
| 64 | + assert get_rc_path("zsh") == "~/.zshrc" | |
| 65 | + | |
| 66 | + def test_fish_returns_config_fish(self): | |
| 67 | + assert "config.fish" in get_rc_path("fish") | |
| 68 | + | |
| 69 | + def test_invalid_shell_raises_value_error(self): | |
| 70 | + with pytest.raises(ValueError, match="Unsupported shell"): | |
| 71 | + get_rc_path("ksh") | |
| 72 | + | |
| 73 | + def test_all_supported_shells_return_string(self): | |
| 74 | + for shell in SUPPORTED_SHELLS: | |
| 75 | + assert isinstance(get_rc_path(shell), str) | |
| 76 | + | |
| 77 | + | |
| 78 | +# ── install_completion ──────────────────────────────────────────────────────── | |
| 79 | + | |
| 80 | + | |
| 81 | +class TestInstallCompletion: | |
| 82 | + def test_creates_file_with_eval_line(self, tmp_path): | |
| 83 | + rc = tmp_path / ".bashrc" | |
| 84 | + result = install_completion("bash", rc_path=str(rc)) | |
| 85 | + assert result.exists() | |
| 86 | + content = rc.read_text() | |
| 87 | + assert "bash_source" in content | |
| 88 | + assert "navegador" in content | |
| 89 | + | |
| 90 | + def test_creates_parent_dirs_for_fish(self, tmp_path): | |
| 91 | + rc = tmp_path / ".config" / "fish" / "config.fish" | |
| 92 | + result = install_completion("fish", rc_path=str(rc)) | |
| 93 | + assert result.exists() | |
| 94 | + assert rc.parent.is_dir() | |
| 95 | + | |
| 96 | + def test_returns_path_object(self, tmp_path): | |
| 97 | + rc = tmp_path / ".zshrc" | |
| 98 | + result = install_completion("zsh", rc_path=str(rc)) | |
| 99 | + assert isinstance(result, Path) | |
| 100 | + | |
| 101 | + def test_idempotent_does_not_duplicate(self, tmp_path): | |
| 102 | + rc = tmp_path / ".bashrc" | |
| 103 | + install_completion("bash", rc_path=str(rc)) | |
| 104 | + install_completion("bash", rc_path=str(rc)) | |
| 105 | + content = rc.read_text() | |
| 106 | + # The eval line should appear exactly once | |
| 107 | + line = "eval \"$(_NAVEGADOR_COMPLETE=bash_source navegador)\"" | |
| 108 | + assert content.count(line) == 1 | |
| 109 | + | |
| 110 | + def test_appends_to_existing_file(self, tmp_path): | |
| 111 | + rc = tmp_path / ".zshrc" | |
| 112 | + rc.write_text("# existing content\nexport FOO=bar\n") | |
| 113 | + install_completion("zsh", rc_path=str(rc)) | |
| 114 | + content = rc.read_text() | |
| 115 | + assert "existing content" in content | |
| 116 | + assert "zsh_source" in content | |
| 117 | + | |
| 118 | + def test_invalid_shell_raises_value_error(self, tmp_path): | |
| 119 | + with pytest.raises(ValueError, match="Unsupported shell"): | |
| 120 | + install_completion("tcsh", rc_path=str(tmp_path / ".tcshrc")) | |
| 121 | + | |
| 122 | + def test_zsh_eval_line_in_file(self, tmp_path): | |
| 123 | + rc = tmp_path / ".zshrc" | |
| 124 | + install_completion("zsh", rc_path=str(rc)) | |
| 125 | + assert "zsh_source" in rc.read_text() | |
| 126 | + | |
| 127 | + def test_fish_source_line_in_file(self, tmp_path): | |
| 128 | + rc = tmp_path / "config.fish" | |
| 129 | + install_completion("fish", rc_path=str(rc)) | |
| 130 | + assert "fish_source" in rc.read_text() | |
| 131 | + assert "| source" in rc.read_text() | |
| 132 | + | |
| 133 | + | |
| 134 | +# ── CLI: navegador completions <shell> ──────────────────────────────────────── | |
| 135 | + | |
| 136 | + | |
| 137 | +class TestCompletionsCommand: | |
| 138 | + # Basic output | |
| 139 | + | |
| 140 | + def test_bash_outputs_eval_line(self): | |
| 141 | + runner = CliRunner() | |
| 142 | + result = runner.invoke(main, ["completions", "bash"]) | |
| 143 | + assert result.exit_code == 0 | |
| 144 | + assert "bash_source" in result.output | |
| 145 | + | |
| 146 | + def test_zsh_outputs_eval_line(self): | |
| 147 | + runner = CliRunner() | |
| 148 | + result = runner.invoke(main, ["completions", "zsh"]) | |
| 149 | + assert result.exit_code == 0 | |
| 150 | + assert "zsh_source" in result.output | |
| 151 | + | |
| 152 | + def test_fish_outputs_source_line(self): | |
| 153 | + runner = CliRunner() | |
| 154 | + result = runner.invoke(main, ["completions", "fish"]) | |
| 155 | + assert result.exit_code == 0 | |
| 156 | + assert "fish_source" in result.output | |
| 157 | + | |
| 158 | + def test_output_mentions_rc_file(self): | |
| 159 | + runner = CliRunner() | |
| 160 | + result = runner.invoke(main, ["completions", "bash"]) | |
| 161 | + assert result.exit_code == 0 | |
| 162 | + assert ".bashrc" in result.output | |
| 163 | + | |
| 164 | + def test_output_mentions_install_hint(self): | |
| 165 | + runner = CliRunner() | |
| 166 | + result = runner.invoke(main, ["completions", "zsh"]) | |
| 167 | + assert result.exit_code == 0 | |
| 168 | + assert "--install" in result.output | |
| 169 | + | |
| 170 | + # Invalid shell | |
| 171 | + | |
| 172 | + def test_invalid_shell_exits_nonzero(self): | |
| 173 | + runner = CliRunner() | |
| 174 | + result = runner.invoke(main, ["completions", "powershell"]) | |
| 175 | + assert result.exit_code != 0 | |
| 176 | + | |
| 177 | + def test_invalid_shell_shows_error(self): | |
| 178 | + runner = CliRunner() | |
| 179 | + result = runner.invoke(main, ["completions", "powershell"]) | |
| 180 | + assert result.exit_code != 0 | |
| 181 | + # Click's Choice type reports the invalid value | |
| 182 | + assert "powershell" in result.output or "invalid" in result.output.lower() | |
| 183 | + | |
| 184 | + # --install flag | |
| 185 | + | |
| 186 | + def test_install_bash_creates_file(self): | |
| 187 | + runner = CliRunner() | |
| 188 | + with runner.isolated_filesystem() as tmp: | |
| 189 | + rc = str(Path(tmp) / ".bashrc") | |
| 190 | + result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc]) | |
| 191 | + assert result.exit_code == 0 | |
| 192 | + assert Path(rc).exists() | |
| 193 | + assert "bash_source" in Path(rc).read_text() | |
| 194 | + | |
| 195 | + def test_install_zsh_creates_file(self): | |
| 196 | + runner = CliRunner() | |
| 197 | + with runner.isolated_filesystem() as tmp: | |
| 198 | + rc = str(Path(tmp) / ".zshrc") | |
| 199 | + result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) | |
| 200 | + assert result.exit_code == 0 | |
| 201 | + assert Path(rc).exists() | |
| 202 | + | |
| 203 | + def test_install_fish_creates_file(self): | |
| 204 | + runner = CliRunner() | |
| 205 | + with runner.isolated_filesystem() as tmp: | |
| 206 | + rc = str(Path(tmp) / "config.fish") | |
| 207 | + result = runner.invoke(main, ["completions", "fish", "--install", "--rc-path", rc]) | |
| 208 | + assert result.exit_code == 0 | |
| 209 | + assert Path(rc).exists() | |
| 210 | + | |
| 211 | + def test_install_shows_success_message(self): | |
| 212 | + runner = CliRunner() | |
| 213 | + with runner.isolated_filesystem() as tmp: | |
| 214 | + rc = str(Path(tmp) / ".bashrc") | |
| 215 | + result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc]) | |
| 216 | + assert result.exit_code == 0 | |
| 217 | + assert "installed" in result.output.lower() or "Completion" in result.output | |
| 218 | + | |
| 219 | + def test_install_shows_restart_hint(self): | |
| 220 | + runner = CliRunner() | |
| 221 | + with runner.isolated_filesystem() as tmp: | |
| 222 | + rc = str(Path(tmp) / ".zshrc") | |
| 223 | + result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) | |
| 224 | + assert result.exit_code == 0 | |
| 225 | + assert "source" in result.output or "restart" in result.output.lower() | |
| 226 | + | |
| 227 | + def test_install_idempotent_via_cli(self): | |
| 228 | + runner = CliRunner() | |
| 229 | + with runner.isolated_filesystem() as tmp: | |
| 230 | + rc = str(Path(tmp) / ".zshrc") | |
| 231 | + runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) | |
| 232 | + runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) | |
| 233 | + content = Path(rc).read_text() | |
| 234 | + line = 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"' | |
| 235 | + assert content.count(line) == 1 |
| --- a/tests/test_completions.py | |
| +++ b/tests/test_completions.py | |
| @@ -0,0 +1,235 @@ | |
| --- a/tests/test_completions.py | |
| +++ b/tests/test_completions.py | |
| @@ -0,0 +1,235 @@ | |
| 1 | """Tests for shell completions — module API and CLI command.""" |
| 2 | |
| 3 | from pathlib import Path |
| 4 | |
| 5 | import pytest |
| 6 | from click.testing import CliRunner |
| 7 | |
| 8 | from navegador.cli.commands import main |
| 9 | from navegador.completions import ( |
| 10 | SUPPORTED_SHELLS, |
| 11 | get_eval_line, |
| 12 | get_rc_path, |
| 13 | install_completion, |
| 14 | ) |
| 15 | |
| 16 | # ── get_eval_line ───────────────────────────────────────────────────────────── |
| 17 | |
| 18 | |
| 19 | class TestGetEvalLine: |
| 20 | def test_bash_contains_bash_source(self): |
| 21 | line = get_eval_line("bash") |
| 22 | assert "bash_source" in line |
| 23 | assert "navegador" in line |
| 24 | |
| 25 | def test_zsh_contains_zsh_source(self): |
| 26 | line = get_eval_line("zsh") |
| 27 | assert "zsh_source" in line |
| 28 | assert "navegador" in line |
| 29 | |
| 30 | def test_fish_contains_fish_source(self): |
| 31 | line = get_eval_line("fish") |
| 32 | assert "fish_source" in line |
| 33 | assert "navegador" in line |
| 34 | |
| 35 | def test_bash_uses_eval(self): |
| 36 | line = get_eval_line("bash") |
| 37 | assert line.startswith("eval ") |
| 38 | |
| 39 | def test_zsh_uses_eval(self): |
| 40 | line = get_eval_line("zsh") |
| 41 | assert line.startswith("eval ") |
| 42 | |
| 43 | def test_fish_uses_pipe_source(self): |
| 44 | line = get_eval_line("fish") |
| 45 | assert "| source" in line |
| 46 | |
| 47 | def test_invalid_shell_raises_value_error(self): |
| 48 | with pytest.raises(ValueError, match="Unsupported shell"): |
| 49 | get_eval_line("powershell") |
| 50 | |
| 51 | def test_all_supported_shells_return_string(self): |
| 52 | for shell in SUPPORTED_SHELLS: |
| 53 | assert isinstance(get_eval_line(shell), str) |
| 54 | |
| 55 | |
| 56 | # ── get_rc_path ─────────────────────────────────────────────────────────────── |
| 57 | |
| 58 | |
| 59 | class TestGetRcPath: |
| 60 | def test_bash_returns_bashrc(self): |
| 61 | assert get_rc_path("bash") == "~/.bashrc" |
| 62 | |
| 63 | def test_zsh_returns_zshrc(self): |
| 64 | assert get_rc_path("zsh") == "~/.zshrc" |
| 65 | |
| 66 | def test_fish_returns_config_fish(self): |
| 67 | assert "config.fish" in get_rc_path("fish") |
| 68 | |
| 69 | def test_invalid_shell_raises_value_error(self): |
| 70 | with pytest.raises(ValueError, match="Unsupported shell"): |
| 71 | get_rc_path("ksh") |
| 72 | |
| 73 | def test_all_supported_shells_return_string(self): |
| 74 | for shell in SUPPORTED_SHELLS: |
| 75 | assert isinstance(get_rc_path(shell), str) |
| 76 | |
| 77 | |
| 78 | # ── install_completion ──────────────────────────────────────────────────────── |
| 79 | |
| 80 | |
| 81 | class TestInstallCompletion: |
| 82 | def test_creates_file_with_eval_line(self, tmp_path): |
| 83 | rc = tmp_path / ".bashrc" |
| 84 | result = install_completion("bash", rc_path=str(rc)) |
| 85 | assert result.exists() |
| 86 | content = rc.read_text() |
| 87 | assert "bash_source" in content |
| 88 | assert "navegador" in content |
| 89 | |
| 90 | def test_creates_parent_dirs_for_fish(self, tmp_path): |
| 91 | rc = tmp_path / ".config" / "fish" / "config.fish" |
| 92 | result = install_completion("fish", rc_path=str(rc)) |
| 93 | assert result.exists() |
| 94 | assert rc.parent.is_dir() |
| 95 | |
| 96 | def test_returns_path_object(self, tmp_path): |
| 97 | rc = tmp_path / ".zshrc" |
| 98 | result = install_completion("zsh", rc_path=str(rc)) |
| 99 | assert isinstance(result, Path) |
| 100 | |
| 101 | def test_idempotent_does_not_duplicate(self, tmp_path): |
| 102 | rc = tmp_path / ".bashrc" |
| 103 | install_completion("bash", rc_path=str(rc)) |
| 104 | install_completion("bash", rc_path=str(rc)) |
| 105 | content = rc.read_text() |
| 106 | # The eval line should appear exactly once |
| 107 | line = "eval \"$(_NAVEGADOR_COMPLETE=bash_source navegador)\"" |
| 108 | assert content.count(line) == 1 |
| 109 | |
| 110 | def test_appends_to_existing_file(self, tmp_path): |
| 111 | rc = tmp_path / ".zshrc" |
| 112 | rc.write_text("# existing content\nexport FOO=bar\n") |
| 113 | install_completion("zsh", rc_path=str(rc)) |
| 114 | content = rc.read_text() |
| 115 | assert "existing content" in content |
| 116 | assert "zsh_source" in content |
| 117 | |
| 118 | def test_invalid_shell_raises_value_error(self, tmp_path): |
| 119 | with pytest.raises(ValueError, match="Unsupported shell"): |
| 120 | install_completion("tcsh", rc_path=str(tmp_path / ".tcshrc")) |
| 121 | |
| 122 | def test_zsh_eval_line_in_file(self, tmp_path): |
| 123 | rc = tmp_path / ".zshrc" |
| 124 | install_completion("zsh", rc_path=str(rc)) |
| 125 | assert "zsh_source" in rc.read_text() |
| 126 | |
| 127 | def test_fish_source_line_in_file(self, tmp_path): |
| 128 | rc = tmp_path / "config.fish" |
| 129 | install_completion("fish", rc_path=str(rc)) |
| 130 | assert "fish_source" in rc.read_text() |
| 131 | assert "| source" in rc.read_text() |
| 132 | |
| 133 | |
| 134 | # ── CLI: navegador completions <shell> ──────────────────────────────────────── |
| 135 | |
| 136 | |
| 137 | class TestCompletionsCommand: |
| 138 | # Basic output |
| 139 | |
| 140 | def test_bash_outputs_eval_line(self): |
| 141 | runner = CliRunner() |
| 142 | result = runner.invoke(main, ["completions", "bash"]) |
| 143 | assert result.exit_code == 0 |
| 144 | assert "bash_source" in result.output |
| 145 | |
| 146 | def test_zsh_outputs_eval_line(self): |
| 147 | runner = CliRunner() |
| 148 | result = runner.invoke(main, ["completions", "zsh"]) |
| 149 | assert result.exit_code == 0 |
| 150 | assert "zsh_source" in result.output |
| 151 | |
| 152 | def test_fish_outputs_source_line(self): |
| 153 | runner = CliRunner() |
| 154 | result = runner.invoke(main, ["completions", "fish"]) |
| 155 | assert result.exit_code == 0 |
| 156 | assert "fish_source" in result.output |
| 157 | |
| 158 | def test_output_mentions_rc_file(self): |
| 159 | runner = CliRunner() |
| 160 | result = runner.invoke(main, ["completions", "bash"]) |
| 161 | assert result.exit_code == 0 |
| 162 | assert ".bashrc" in result.output |
| 163 | |
| 164 | def test_output_mentions_install_hint(self): |
| 165 | runner = CliRunner() |
| 166 | result = runner.invoke(main, ["completions", "zsh"]) |
| 167 | assert result.exit_code == 0 |
| 168 | assert "--install" in result.output |
| 169 | |
| 170 | # Invalid shell |
| 171 | |
| 172 | def test_invalid_shell_exits_nonzero(self): |
| 173 | runner = CliRunner() |
| 174 | result = runner.invoke(main, ["completions", "powershell"]) |
| 175 | assert result.exit_code != 0 |
| 176 | |
| 177 | def test_invalid_shell_shows_error(self): |
| 178 | runner = CliRunner() |
| 179 | result = runner.invoke(main, ["completions", "powershell"]) |
| 180 | assert result.exit_code != 0 |
| 181 | # Click's Choice type reports the invalid value |
| 182 | assert "powershell" in result.output or "invalid" in result.output.lower() |
| 183 | |
| 184 | # --install flag |
| 185 | |
| 186 | def test_install_bash_creates_file(self): |
| 187 | runner = CliRunner() |
| 188 | with runner.isolated_filesystem() as tmp: |
| 189 | rc = str(Path(tmp) / ".bashrc") |
| 190 | result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc]) |
| 191 | assert result.exit_code == 0 |
| 192 | assert Path(rc).exists() |
| 193 | assert "bash_source" in Path(rc).read_text() |
| 194 | |
| 195 | def test_install_zsh_creates_file(self): |
| 196 | runner = CliRunner() |
| 197 | with runner.isolated_filesystem() as tmp: |
| 198 | rc = str(Path(tmp) / ".zshrc") |
| 199 | result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) |
| 200 | assert result.exit_code == 0 |
| 201 | assert Path(rc).exists() |
| 202 | |
| 203 | def test_install_fish_creates_file(self): |
| 204 | runner = CliRunner() |
| 205 | with runner.isolated_filesystem() as tmp: |
| 206 | rc = str(Path(tmp) / "config.fish") |
| 207 | result = runner.invoke(main, ["completions", "fish", "--install", "--rc-path", rc]) |
| 208 | assert result.exit_code == 0 |
| 209 | assert Path(rc).exists() |
| 210 | |
| 211 | def test_install_shows_success_message(self): |
| 212 | runner = CliRunner() |
| 213 | with runner.isolated_filesystem() as tmp: |
| 214 | rc = str(Path(tmp) / ".bashrc") |
| 215 | result = runner.invoke(main, ["completions", "bash", "--install", "--rc-path", rc]) |
| 216 | assert result.exit_code == 0 |
| 217 | assert "installed" in result.output.lower() or "Completion" in result.output |
| 218 | |
| 219 | def test_install_shows_restart_hint(self): |
| 220 | runner = CliRunner() |
| 221 | with runner.isolated_filesystem() as tmp: |
| 222 | rc = str(Path(tmp) / ".zshrc") |
| 223 | result = runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) |
| 224 | assert result.exit_code == 0 |
| 225 | assert "source" in result.output or "restart" in result.output.lower() |
| 226 | |
| 227 | def test_install_idempotent_via_cli(self): |
| 228 | runner = CliRunner() |
| 229 | with runner.isolated_filesystem() as tmp: |
| 230 | rc = str(Path(tmp) / ".zshrc") |
| 231 | runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) |
| 232 | runner.invoke(main, ["completions", "zsh", "--install", "--rc-path", rc]) |
| 233 | content = Path(rc).read_text() |
| 234 | line = 'eval "$(_NAVEGADOR_COMPLETE=zsh_source navegador)"' |
| 235 | assert content.count(line) == 1 |