mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -07:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cad496650b | |||
| 38de78045d | |||
| dce25e4229 | |||
| 59bf8cbb45 | |||
| 4e80f01fda | |||
| 6c78eb7a39 | |||
| 86a3a913b5 | |||
| 05e4ea80ee | |||
| cca595971d | |||
| 26b9af1cdc | |||
| b459e1a108 | |||
| 62cb14fa52 | |||
| 7a65c1e91d | |||
| 5a3c7154df | |||
| b9c87c14a2 | |||
| 52250c162d |
@@ -30,7 +30,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
## 📦 Installation
|
||||
|
||||
See
|
||||
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
|
||||
[Gemini CLI installation, execution, and releases](./docs/resources/installation.md)
|
||||
for recommended system specifications and a detailed installation guide.
|
||||
|
||||
### Quick Install
|
||||
@@ -209,7 +209,7 @@ gemini
|
||||
```
|
||||
|
||||
For Google Workspace accounts and other authentication methods, see the
|
||||
[authentication guide](./docs/get-started/authentication.md).
|
||||
[authentication guide](./docs/resources/authentication.md).
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
@@ -280,8 +280,8 @@ gemini
|
||||
|
||||
- [**Quickstart Guide**](./docs/get-started/index.md) - Get up and running
|
||||
quickly.
|
||||
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
|
||||
auth configuration.
|
||||
- [**Authentication Setup**](./docs/resources/authentication.md) - Detailed auth
|
||||
configuration.
|
||||
- [**Configuration Guide**](./docs/reference/configuration.md) - Settings and
|
||||
customization.
|
||||
- [**Keyboard Shortcuts**](./docs/reference/keyboard-shortcuts.md) -
|
||||
|
||||
@@ -50,6 +50,7 @@ These commands are available within the interactive REPL.
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. Forces non-interactive mode. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--worktree` | `-w` | string | - | Start Gemini in a new git worktree. If no name is provided, one is generated automatically. Requires `experimental.worktrees: true` in settings. |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
|
||||
@@ -508,7 +508,7 @@ other events. For more information, see the
|
||||
You can enforce a specific authentication method for all users by setting the
|
||||
`enforcedAuthType` in the system-level `settings.json` file. This prevents users
|
||||
from choosing a different authentication method. See the
|
||||
[Authentication docs](../get-started/authentication.md) for more details.
|
||||
[Authentication docs](../resources/authentication.md) for more details.
|
||||
|
||||
**Example:** Enforce the use of Google login for all users.
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# Git Worktrees (experimental)
|
||||
|
||||
When working on multiple tasks at once, you can use Git worktrees to give each
|
||||
Gemini session its own copy of the codebase. Git worktrees create separate
|
||||
working directories that each have their own files and branch while sharing the
|
||||
same repository history. This prevents changes in one session from colliding
|
||||
with another.
|
||||
|
||||
Learn more about [session management](./session-management.md).
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development. Your
|
||||
> feedback is invaluable as we refine this feature. If you have ideas,
|
||||
> suggestions, or encounter issues:
|
||||
>
|
||||
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml) on GitHub.
|
||||
> - Use the **/bug** command within Gemini CLI to file an issue.
|
||||
|
||||
Learn more in the official Git worktree
|
||||
[documentation](https://git-scm.com/docs/git-worktree).
|
||||
|
||||
## How to enable Git worktrees
|
||||
|
||||
Git worktrees are an experimental feature. You must enable them in your settings
|
||||
using the `/settings` command or by manually editing your `settings.json` file.
|
||||
|
||||
1. Use the `/settings` command.
|
||||
2. Search for and set **Enable Git Worktrees** to `true`.
|
||||
|
||||
Alternatively, add the following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"worktrees": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How to use Git worktrees
|
||||
|
||||
Use the `--worktree` (`-w`) flag to create an isolated worktree and start Gemini
|
||||
CLI in it.
|
||||
|
||||
- **Start with a specific name:** The value you pass becomes both the directory
|
||||
name (within `.gemini/worktrees/`) and the branch name.
|
||||
|
||||
```bash
|
||||
gemini --worktree feature-search
|
||||
```
|
||||
|
||||
- **Start with a random name:** If you omit the name, Gemini generates a random
|
||||
one automatically (for example, `worktree-a1b2c3d4`).
|
||||
|
||||
```bash
|
||||
gemini --worktree
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Remember to initialize your development environment in each new
|
||||
> worktree according to your project's setup. Depending on your stack, this
|
||||
> might include running dependency installation (`npm install`, `yarn`), setting
|
||||
> up virtual environments, or following your project's standard build process.
|
||||
|
||||
## How to exit a Git worktree session
|
||||
|
||||
When you exit a worktree session (using `/quit` or `Ctrl+C`), Gemini leaves the
|
||||
worktree intact so your work is not lost. This includes your uncommitted changes
|
||||
(modified files, staged changes, or untracked files) and any new commits you
|
||||
have made.
|
||||
|
||||
Gemini prioritizes a fast and safe exit: it **does not automatically delete**
|
||||
your worktree or branch. You are responsible for cleaning up your worktrees
|
||||
manually once you are finished with them.
|
||||
|
||||
When you exit, Gemini displays instructions on how to resume your work or how to
|
||||
manually remove the worktree if you no longer need it.
|
||||
|
||||
## Resuming work in a Git worktree
|
||||
|
||||
To resume a session in a worktree, navigate to the worktree directory and start
|
||||
Gemini CLI with the `--resume` flag and the session ID:
|
||||
|
||||
```bash
|
||||
cd .gemini/worktrees/feature-search
|
||||
gemini --resume <session_id>
|
||||
```
|
||||
|
||||
## Managing Git worktrees manually
|
||||
|
||||
For more control over worktree location and branch configuration, or to clean up
|
||||
a preserved worktree, you can use Git directly:
|
||||
|
||||
- **Clean up a preserved Git worktree:**
|
||||
```bash
|
||||
git worktree remove .gemini/worktrees/feature-search --force
|
||||
git branch -D worktree-feature-search
|
||||
```
|
||||
- **Create a Git worktree manually:**
|
||||
```bash
|
||||
git worktree add ../project-feature-search -b feature-search
|
||||
cd ../project-feature-search && gemini
|
||||
```
|
||||
|
||||
[Open an issue]: https://github.com/google-gemini/gemini-cli/issues
|
||||
@@ -96,6 +96,12 @@ Compatibility aliases:
|
||||
- `/chat ...` works for the same commands.
|
||||
- `/resume checkpoints ...` also remains supported during migration.
|
||||
|
||||
## Parallel sessions with Git worktrees
|
||||
|
||||
When working on multiple tasks at once, you can use
|
||||
[Git worktrees](./git-worktrees.md) to give each Gemini session its own copy of
|
||||
the codebase. This prevents changes in one session from colliding with another.
|
||||
|
||||
## Managing sessions
|
||||
|
||||
You can list and delete sessions to keep your history organized and manage disk
|
||||
|
||||
@@ -151,6 +151,7 @@ they appear in the UI.
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
|
||||
|
||||
@@ -24,7 +24,7 @@ project-specific behavior or create a customized persona.
|
||||
|
||||
You can set the environment variable temporarily in your shell, or persist it
|
||||
via a `.gemini/.env` file. See
|
||||
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
|
||||
[Persisting Environment Variables](../resources/authentication.md#persisting-environment-variables).
|
||||
|
||||
- Use the project default path (`.gemini/system.md`):
|
||||
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
|
||||
|
||||
@@ -306,6 +306,7 @@ Emitted at startup with the CLI configuration.
|
||||
- `extension_ids` (string)
|
||||
- `extensions_count` (int)
|
||||
- `auth_type` (string)
|
||||
- `worktree_active` (boolean)
|
||||
- `github_workflow_name` (string, optional)
|
||||
- `github_repository_hash` (string, optional)
|
||||
- `github_event_name` (string, optional)
|
||||
|
||||
@@ -23,7 +23,7 @@ Gemini CLI creates a copy of the extension during installation. You must run
|
||||
GitHub, you must have `git` installed on your machine.
|
||||
|
||||
```bash
|
||||
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent]
|
||||
gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release] [--consent] [--skip-settings]
|
||||
```
|
||||
|
||||
- `<source>`: The GitHub URL or local path of the extension.
|
||||
@@ -31,6 +31,7 @@ gemini extensions install <source> [--ref <ref>] [--auto-update] [--pre-release]
|
||||
- `--auto-update`: Enable automatic updates for this extension.
|
||||
- `--pre-release`: Enable installation of pre-release versions.
|
||||
- `--consent`: Acknowledge security risks and skip the confirmation prompt.
|
||||
- `--skip-settings`: Skip the configuration on install process.
|
||||
|
||||
### Uninstall an extension
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
|
||||
gemini
|
||||
```
|
||||
|
||||
For more installation options, see [Gemini CLI Installation](./installation.md).
|
||||
For more installation options, see
|
||||
[Gemini CLI Installation](../resources/installation.md).
|
||||
|
||||
## Authenticate
|
||||
|
||||
@@ -46,7 +47,7 @@ cases, you can log in with your existing Google account:
|
||||
|
||||
Certain account types may require you to configure a Google Cloud project. For
|
||||
more information, including other authentication methods, see
|
||||
[Gemini CLI Authentication Setup](./authentication.md).
|
||||
[Gemini CLI Authentication Setup](../resources/authentication.md).
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
+7
-4
@@ -10,15 +10,15 @@ context.
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
For more installation options and authentication setup, see the full
|
||||
[Installation](./resources/installation.md) and
|
||||
[Authentication](./resources/authentication.md) guides.
|
||||
|
||||
## Get started
|
||||
|
||||
Jump in to Gemini CLI.
|
||||
|
||||
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
|
||||
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
|
||||
on your system.
|
||||
- **[Authentication](./get-started/authentication.md):** Setup instructions for
|
||||
personal and enterprise accounts.
|
||||
- **[Examples](./get-started/examples.md):** Practical examples of Gemini CLI in
|
||||
action.
|
||||
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
|
||||
@@ -116,6 +116,9 @@ Deep technical documentation and API specifications.
|
||||
Support, release history, and legal information.
|
||||
|
||||
- **[FAQ](./resources/faq.md):** Answers to frequently asked questions.
|
||||
- **[Installation](./resources/installation.md):** How to install Gemini CLI.
|
||||
- **[Authentication](./resources/authentication.md):** Setup instructions for
|
||||
personal and enterprise accounts.
|
||||
- **[Quota and pricing](./resources/quota-and-pricing.md):** Limits and billing
|
||||
details.
|
||||
- **[Terms and privacy](./resources/tos-privacy.md):** Official notices and
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
"/docs/core/tools-api": "/docs/reference/tools",
|
||||
"/docs/reference/tools-api": "/docs/reference/tools",
|
||||
"/docs/faq": "/docs/resources/faq",
|
||||
"/docs/get-started/authentication": "/docs/resources/authentication",
|
||||
"/docs/get-started/configuration": "/docs/reference/configuration",
|
||||
"/docs/get-started/configuration-v1": "/docs/reference/configuration",
|
||||
"/docs/get-started/installation": "/docs/resources/installation",
|
||||
"/docs/index": "/docs",
|
||||
"/docs/quota-and-pricing": "/docs/resources/quota-and-pricing",
|
||||
"/docs/tos-privacy": "/docs/resources/tos-privacy",
|
||||
|
||||
@@ -1527,6 +1527,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.worktrees`** (boolean):
|
||||
- **Description:** Enable automated Git worktree management for parallel work.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.extensionManagement`** (boolean):
|
||||
- **Description:** Enable extension management features.
|
||||
- **Default:** `true`
|
||||
@@ -1898,8 +1903,8 @@ within your user's home folder.
|
||||
Environment variables are a common way to configure applications, especially for
|
||||
sensitive information like API keys or for settings that might change between
|
||||
environments. For authentication setup, see the
|
||||
[Authentication documentation](../get-started/authentication.md) which covers
|
||||
all available authentication methods.
|
||||
[Authentication documentation](../resources/authentication.md) which covers all
|
||||
available authentication methods.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The
|
||||
loading order is:
|
||||
@@ -1919,7 +1924,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
- **`GEMINI_API_KEY`**:
|
||||
- Your API key for the Gemini API.
|
||||
- One of several available
|
||||
[authentication methods](../get-started/authentication.md).
|
||||
[authentication methods](../resources/authentication.md).
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env`
|
||||
file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
|
||||
+10
-5
@@ -7,11 +7,6 @@
|
||||
"items": [
|
||||
{ "label": "Overview", "slug": "docs" },
|
||||
{ "label": "Quickstart", "slug": "docs/get-started" },
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
|
||||
{
|
||||
@@ -99,6 +94,11 @@
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{
|
||||
"label": "Git worktrees",
|
||||
"badge": "🔬",
|
||||
"slug": "docs/cli/git-worktrees"
|
||||
},
|
||||
{
|
||||
"label": "Hooks",
|
||||
"collapsed": true,
|
||||
@@ -215,6 +215,11 @@
|
||||
"label": "Resources",
|
||||
"items": [
|
||||
{ "label": "FAQ", "slug": "docs/resources/faq" },
|
||||
{ "label": "Installation", "slug": "docs/resources/installation" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/resources/authentication"
|
||||
},
|
||||
{
|
||||
"label": "Quota and pricing",
|
||||
"slug": "docs/resources/quota-and-pricing"
|
||||
|
||||
@@ -12,48 +12,46 @@ import {
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type MockInstance,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { handleInstall, installCommand } from './install.js';
|
||||
import yargs from 'yargs';
|
||||
import * as core from '@google/gemini-cli-core';
|
||||
import {
|
||||
ExtensionManager,
|
||||
type inferInstallMetadata,
|
||||
} from '../../config/extension-manager.js';
|
||||
import type {
|
||||
promptForConsentNonInteractive,
|
||||
requestConsentNonInteractive,
|
||||
} from '../../config/extensions/consent.js';
|
||||
import type {
|
||||
isWorkspaceTrusted,
|
||||
loadTrustedFolders,
|
||||
} from '../../config/trustedFolders.js';
|
||||
import type * as fs from 'node:fs/promises';
|
||||
import type { Stats } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
|
||||
|
||||
const mockInstallOrUpdateExtension: Mock<
|
||||
typeof ExtensionManager.prototype.installOrUpdateExtension
|
||||
> = vi.hoisted(() => vi.fn());
|
||||
const mockRequestConsentNonInteractive: Mock<
|
||||
typeof requestConsentNonInteractive
|
||||
> = vi.hoisted(() => vi.fn());
|
||||
const mockPromptForConsentNonInteractive: Mock<
|
||||
typeof promptForConsentNonInteractive
|
||||
> = vi.hoisted(() => vi.fn());
|
||||
const mockStat: Mock<typeof fs.stat> = vi.hoisted(() => vi.fn());
|
||||
const mockInferInstallMetadata: Mock<typeof inferInstallMetadata> = vi.hoisted(
|
||||
() => vi.fn(),
|
||||
);
|
||||
const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() =>
|
||||
vi.fn(),
|
||||
);
|
||||
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
|
||||
vi.fn(),
|
||||
);
|
||||
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> =
|
||||
vi.hoisted(() => vi.fn());
|
||||
const {
|
||||
mockInstallOrUpdateExtension,
|
||||
mockLoadExtensions,
|
||||
mockExtensionManager,
|
||||
mockRequestConsentNonInteractive,
|
||||
mockPromptForConsentNonInteractive,
|
||||
mockStat,
|
||||
mockInferInstallMetadata,
|
||||
mockIsWorkspaceTrusted,
|
||||
mockLoadTrustedFolders,
|
||||
mockDiscover,
|
||||
} = vi.hoisted(() => {
|
||||
const mockLoadExtensions = vi.fn();
|
||||
const mockInstallOrUpdateExtension = vi.fn();
|
||||
const mockExtensionManager = vi.fn().mockImplementation(() => ({
|
||||
loadExtensions: mockLoadExtensions,
|
||||
installOrUpdateExtension: mockInstallOrUpdateExtension,
|
||||
}));
|
||||
|
||||
return {
|
||||
mockLoadExtensions,
|
||||
mockInstallOrUpdateExtension,
|
||||
mockExtensionManager,
|
||||
mockRequestConsentNonInteractive: vi.fn(),
|
||||
mockPromptForConsentNonInteractive: vi.fn(),
|
||||
mockStat: vi.fn(),
|
||||
mockInferInstallMetadata: vi.fn(),
|
||||
mockIsWorkspaceTrusted: vi.fn(),
|
||||
mockLoadTrustedFolders: vi.fn(),
|
||||
mockDiscover: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: mockRequestConsentNonInteractive,
|
||||
@@ -84,6 +82,7 @@ vi.mock('../../config/extension-manager.js', async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import('../../config/extension-manager.js')
|
||||
>()),
|
||||
ExtensionManager: mockExtensionManager,
|
||||
inferInstallMetadata: mockInferInstallMetadata,
|
||||
}));
|
||||
|
||||
@@ -117,19 +116,18 @@ describe('handleInstall', () => {
|
||||
let processSpy: MockInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
debugLogSpy = vi.spyOn(core.debugLogger, 'log');
|
||||
debugErrorSpy = vi.spyOn(core.debugLogger, 'error');
|
||||
debugLogSpy = vi
|
||||
.spyOn(core.debugLogger, 'log')
|
||||
.mockImplementation(() => {});
|
||||
debugErrorSpy = vi
|
||||
.spyOn(core.debugLogger, 'error')
|
||||
.mockImplementation(() => {});
|
||||
processSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
|
||||
vi.spyOn(ExtensionManager.prototype, 'loadExtensions').mockResolvedValue(
|
||||
[],
|
||||
);
|
||||
vi.spyOn(
|
||||
ExtensionManager.prototype,
|
||||
'installOrUpdateExtension',
|
||||
).mockImplementation(mockInstallOrUpdateExtension);
|
||||
mockLoadExtensions.mockResolvedValue([]);
|
||||
mockInstallOrUpdateExtension.mockReset();
|
||||
|
||||
mockIsWorkspaceTrusted.mockReturnValue({ isTrusted: true, source: 'file' });
|
||||
mockDiscover.mockResolvedValue({
|
||||
@@ -163,12 +161,7 @@ describe('handleInstall', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockInstallOrUpdateExtension.mockClear();
|
||||
mockRequestConsentNonInteractive.mockClear();
|
||||
mockStat.mockClear();
|
||||
mockInferInstallMetadata.mockClear();
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createMockExtension(
|
||||
@@ -288,6 +281,39 @@ describe('handleInstall', () => {
|
||||
expect(processSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should pass promptForSetting when skipSettings is not provided', async () => {
|
||||
mockInstallOrUpdateExtension.mockResolvedValue({
|
||||
name: 'test-extension',
|
||||
} as unknown as core.GeminiCLIExtension);
|
||||
|
||||
await handleInstall({
|
||||
source: 'http://google.com',
|
||||
});
|
||||
|
||||
expect(mockExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestSetting: promptForSetting,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass null for requestSetting when skipSettings is true', async () => {
|
||||
mockInstallOrUpdateExtension.mockResolvedValue({
|
||||
name: 'test-extension',
|
||||
} as unknown as core.GeminiCLIExtension);
|
||||
|
||||
await handleInstall({
|
||||
source: 'http://google.com',
|
||||
skipSettings: true,
|
||||
});
|
||||
|
||||
expect(mockExtensionManager).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestSetting: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should proceed if local path is already trusted', async () => {
|
||||
mockInstallOrUpdateExtension.mockResolvedValue(
|
||||
createMockExtension({
|
||||
|
||||
@@ -37,6 +37,7 @@ interface InstallArgs {
|
||||
autoUpdate?: boolean;
|
||||
allowPreRelease?: boolean;
|
||||
consent?: boolean;
|
||||
skipSettings?: boolean;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
@@ -153,7 +154,7 @@ export async function handleInstall(args: InstallArgs) {
|
||||
const extensionManager = new ExtensionManager({
|
||||
workspaceDir,
|
||||
requestConsent,
|
||||
requestSetting: promptForSetting,
|
||||
requestSetting: args.skipSettings ? null : promptForSetting,
|
||||
settings,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
@@ -196,6 +197,11 @@ export const installCommand: CommandModule = {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.option('skip-settings', {
|
||||
describe: 'Skip the configuration on install process.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.source) {
|
||||
throw new Error('The source argument must be provided.');
|
||||
@@ -214,6 +220,8 @@ export const installCommand: CommandModule = {
|
||||
allowPreRelease: argv['pre-release'] as boolean | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
consent: argv['consent'] as boolean | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
skipSettings: argv['skip-settings'] as boolean | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
|
||||
@@ -226,6 +226,51 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('parseArguments', () => {
|
||||
describe('worktree', () => {
|
||||
it('should parse --worktree flag when provided with a name', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree', 'my-feature'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = true;
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.worktree).toBe('my-feature');
|
||||
});
|
||||
|
||||
it('should generate a random name when --worktree is provided without a name', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = true;
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.worktree).toBeDefined();
|
||||
expect(argv.worktree).not.toBe('');
|
||||
expect(typeof argv.worktree).toBe('string');
|
||||
});
|
||||
|
||||
it('should throw an error when --worktree is used but experimental.worktrees is not enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--worktree', 'feature'];
|
||||
const settings = createTestMergedSettings();
|
||||
settings.experimental.worktrees = false;
|
||||
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await expect(parseArguments(settings)).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'The --worktree flag is only available when experimental.worktrees is enabled in your settings.',
|
||||
),
|
||||
);
|
||||
|
||||
mockExit.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description: 'long flags',
|
||||
@@ -2225,6 +2270,30 @@ describe('loadCliConfig tool exclusions', () => {
|
||||
expect(config.getExcludeTools()).toContain('ask_user');
|
||||
});
|
||||
|
||||
it('should exclude ask_user in interactive mode when --acp is provided', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--acp'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getExcludeTools()).toContain('ask_user');
|
||||
});
|
||||
|
||||
it('should exclude ask_user in interactive mode when --experimental-acp is provided', async () => {
|
||||
process.stdin.isTTY = true;
|
||||
process.argv = ['node', 'script.js', '--experimental-acp'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const config = await loadCliConfig(
|
||||
createTestMergedSettings(),
|
||||
'test-session',
|
||||
argv,
|
||||
);
|
||||
expect(config.getExcludeTools()).toContain('ask_user');
|
||||
});
|
||||
|
||||
it('should not exclude shell tool in non-interactive mode when --allowed-tools="ShellTool" is set', async () => {
|
||||
process.stdin.isTTY = false;
|
||||
process.argv = [
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import yargs from 'yargs/yargs';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { execa } from 'execa';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
@@ -38,6 +39,9 @@ import {
|
||||
applyAdminAllowlist,
|
||||
applyRequiredServers,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
getProjectRootForWorktree,
|
||||
isGeminiWorktree,
|
||||
type WorktreeSettings,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
@@ -48,6 +52,8 @@ import {
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
isWorktreeEnabled,
|
||||
type LoadedSettings,
|
||||
} from './settings.js';
|
||||
|
||||
import { loadSandboxConfig } from './sandboxConfig.js';
|
||||
@@ -74,6 +80,7 @@ export interface CliArgs {
|
||||
debug: boolean | undefined;
|
||||
prompt: string | undefined;
|
||||
promptInteractive: string | undefined;
|
||||
worktree?: string;
|
||||
|
||||
yolo: boolean | undefined;
|
||||
approvalMode: string | undefined;
|
||||
@@ -115,6 +122,36 @@ const coerceCommaSeparated = (values: string[]): string[] => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pre-parses the command line arguments to find the worktree flag.
|
||||
* Used for early setup before full argument parsing with settings.
|
||||
*/
|
||||
export function getWorktreeArg(argv: string[]): string | undefined {
|
||||
const result = yargs(hideBin(argv))
|
||||
.help(false)
|
||||
.version(false)
|
||||
.option('worktree', { alias: 'w', type: 'string' })
|
||||
.strict(false)
|
||||
.exitProcess(false)
|
||||
.parseSync();
|
||||
|
||||
if (result.worktree === undefined) return undefined;
|
||||
return typeof result.worktree === 'string' ? result.worktree.trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a worktree is requested via CLI and enabled in settings.
|
||||
* Returns the requested name (can be empty string for auto-generated) or undefined.
|
||||
*/
|
||||
export function getRequestedWorktreeName(
|
||||
settings: LoadedSettings,
|
||||
): string | undefined {
|
||||
if (!isWorktreeEnabled(settings)) {
|
||||
return undefined;
|
||||
}
|
||||
return getWorktreeArg(process.argv);
|
||||
}
|
||||
|
||||
export async function parseArguments(
|
||||
settings: MergedSettings,
|
||||
): Promise<CliArgs> {
|
||||
@@ -158,6 +195,20 @@ export async function parseArguments(
|
||||
description:
|
||||
'Execute the provided prompt and continue in interactive mode',
|
||||
})
|
||||
.option('worktree', {
|
||||
alias: 'w',
|
||||
type: 'string',
|
||||
skipValidation: true,
|
||||
description:
|
||||
'Start Gemini in a new git worktree. If no name is provided, one is generated automatically.',
|
||||
coerce: (value: unknown): string => {
|
||||
const trimmed = typeof value === 'string' ? value.trim() : '';
|
||||
if (trimmed === '') {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
return trimmed;
|
||||
},
|
||||
})
|
||||
.option('sandbox', {
|
||||
alias: 's',
|
||||
type: 'boolean',
|
||||
@@ -335,6 +386,9 @@ export async function parseArguments(
|
||||
) {
|
||||
return `Invalid values:\n Argument: output-format, Given: "${argv['outputFormat']}", Choices: "text", "json", "stream-json"`;
|
||||
}
|
||||
if (argv['worktree'] && !settings.experimental?.worktrees) {
|
||||
return 'The --worktree flag is only available when experimental.worktrees is enabled in your settings.';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -420,6 +474,7 @@ export interface LoadCliConfigOptions {
|
||||
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & {
|
||||
disabled?: string[];
|
||||
};
|
||||
worktreeSettings?: WorktreeSettings;
|
||||
}
|
||||
|
||||
export async function loadCliConfig(
|
||||
@@ -431,6 +486,9 @@ export async function loadCliConfig(
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const worktreeSettings =
|
||||
options.worktreeSettings ?? (await resolveWorktreeSettings(cwd));
|
||||
|
||||
if (argv.sandbox) {
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
}
|
||||
@@ -649,12 +707,16 @@ export async function loadCliConfig(
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
|
||||
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
|
||||
|
||||
// In non-interactive mode, exclude tools that require a prompt.
|
||||
const extraExcludes: string[] = [];
|
||||
if (!interactive) {
|
||||
if (!interactive || isAcpMode) {
|
||||
// The Policy Engine natively handles headless safety by translating ASK_USER
|
||||
// decisions to DENY. However, we explicitly block ask_user here to guarantee
|
||||
// it can never be allowed via a high-priority policy rule when no human is present.
|
||||
// We also exclude it in ACP mode as IDEs intercept tool calls and ask for permission,
|
||||
// breaking conversational flows.
|
||||
extraExcludes.push(ASK_USER_TOOL_NAME);
|
||||
}
|
||||
|
||||
@@ -770,7 +832,6 @@ export async function loadCliConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const isAcpMode = !!argv.acp || !!argv.experimentalAcp;
|
||||
let clientName: string | undefined = undefined;
|
||||
if (isAcpMode) {
|
||||
const ide = detectIdeFromEnv();
|
||||
@@ -799,6 +860,7 @@ export async function loadCliConfig(
|
||||
importFormat: settings.context?.importFormat,
|
||||
debugMode,
|
||||
question,
|
||||
worktreeSettings,
|
||||
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
@@ -940,3 +1002,48 @@ function mergeExcludeTools(
|
||||
]);
|
||||
return Array.from(allExcludeTools);
|
||||
}
|
||||
|
||||
async function resolveWorktreeSettings(
|
||||
cwd: string,
|
||||
): Promise<WorktreeSettings | undefined> {
|
||||
let worktreePath: string | undefined;
|
||||
try {
|
||||
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel'], {
|
||||
cwd,
|
||||
});
|
||||
const toplevel = stdout.trim();
|
||||
const projectRoot = await getProjectRootForWorktree(toplevel);
|
||||
|
||||
if (isGeminiWorktree(toplevel, projectRoot)) {
|
||||
worktreePath = toplevel;
|
||||
}
|
||||
} catch (_e) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!worktreePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let worktreeBaseSha: string | undefined;
|
||||
try {
|
||||
const { stdout } = await execa('git', ['rev-parse', 'HEAD'], {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
worktreeBaseSha = stdout.trim();
|
||||
} catch (e: unknown) {
|
||||
debugLogger.debug(
|
||||
`Failed to resolve worktree base SHA at ${worktreePath}: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!worktreeBaseSha) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: path.basename(worktreePath),
|
||||
path: worktreePath,
|
||||
baseSha: worktreeBaseSha,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
});
|
||||
|
||||
async function expectConsentSnapshot(consentString: string) {
|
||||
const renderResult = render(React.createElement(Text, null, consentString));
|
||||
await renderResult.waitUntilReady();
|
||||
const renderResult = await render(
|
||||
React.createElement(Text, null, consentString),
|
||||
);
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
}
|
||||
|
||||
|
||||
@@ -632,6 +632,10 @@ export function resetSettingsCacheForTesting() {
|
||||
settingsCache.clear();
|
||||
}
|
||||
|
||||
export function isWorktreeEnabled(settings: LoadedSettings): boolean {
|
||||
return settings.merged.experimental.worktrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads settings from user and workspace directories.
|
||||
* Project settings override user settings.
|
||||
|
||||
@@ -538,8 +538,32 @@ describe('SettingsSchema', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const visitJsonSchema = (jsonSchema: Record<string, unknown>) => {
|
||||
const ref = jsonSchema['ref'];
|
||||
if (typeof ref === 'string') {
|
||||
referenced.add(ref);
|
||||
}
|
||||
const properties = jsonSchema['properties'];
|
||||
if (
|
||||
properties &&
|
||||
typeof properties === 'object' &&
|
||||
!Array.isArray(properties)
|
||||
) {
|
||||
Object.values(properties as Record<string, unknown>).forEach((prop) =>
|
||||
visitJsonSchema(prop as Record<string, unknown>),
|
||||
);
|
||||
}
|
||||
const items = jsonSchema['items'];
|
||||
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
||||
visitJsonSchema(items as Record<string, unknown>);
|
||||
}
|
||||
};
|
||||
|
||||
Object.values(schema).forEach(visitDefinition);
|
||||
|
||||
// Also visit all definitions to find nested references
|
||||
Object.values(SETTINGS_SCHEMA_DEFINITIONS).forEach(visitJsonSchema);
|
||||
|
||||
// Ensure definitions map doesn't accumulate stale entries.
|
||||
Object.keys(SETTINGS_SCHEMA_DEFINITIONS).forEach((key) => {
|
||||
if (!referenced.has(key)) {
|
||||
|
||||
@@ -1094,7 +1094,7 @@ const SETTINGS_SCHEMA = {
|
||||
showInDialog: false,
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
ref: 'ModelPolicy',
|
||||
ref: 'ModelPolicyChain',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1906,6 +1906,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable local and remote subagents.',
|
||||
showInDialog: false,
|
||||
},
|
||||
worktrees: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Git Worktrees',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable automated Git worktree management for parallel work.',
|
||||
showInDialog: true,
|
||||
},
|
||||
extensionManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Extension Management',
|
||||
@@ -2988,6 +2998,14 @@ export const SETTINGS_SCHEMA_DEFINITIONS: Record<
|
||||
},
|
||||
},
|
||||
},
|
||||
ModelPolicyChain: {
|
||||
type: 'array',
|
||||
description: 'A chain of model policies for fallback behavior.',
|
||||
items: {
|
||||
type: 'object',
|
||||
ref: 'ModelPolicy',
|
||||
},
|
||||
},
|
||||
ModelPolicy: {
|
||||
type: 'object',
|
||||
description:
|
||||
|
||||
@@ -199,6 +199,8 @@ vi.mock('./config/config.js', () => ({
|
||||
networkAccess: false,
|
||||
}),
|
||||
isDebugMode: vi.fn(() => false),
|
||||
getRequestedWorktreeName: vi.fn(() => undefined),
|
||||
getWorktreeArg: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('read-package-up', () => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
WarningPriority,
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
type WorktreeInfo,
|
||||
type OutputPayload,
|
||||
type ConsoleLogPayload,
|
||||
type UserFeedbackPayload,
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
registerTelemetryConfig,
|
||||
setupSignalHandlers,
|
||||
} from './utils/cleanup.js';
|
||||
import { setupWorktree } from './utils/worktreeSetup.js';
|
||||
import {
|
||||
cleanupToolOutputFiles,
|
||||
cleanupExpiredSessions,
|
||||
@@ -210,6 +212,13 @@ export async function main() {
|
||||
const settings = loadSettings();
|
||||
loadSettingsHandle?.end();
|
||||
|
||||
// If a worktree is requested and enabled, set it up early.
|
||||
const requestedWorktree = cliConfig.getRequestedWorktreeName(settings);
|
||||
let worktreeInfo: WorktreeInfo | undefined;
|
||||
if (requestedWorktree !== undefined) {
|
||||
worktreeInfo = await setupWorktree(requestedWorktree || undefined);
|
||||
}
|
||||
|
||||
// Report settings errors once during startup
|
||||
settings.errors.forEach((error) => {
|
||||
coreEvents.emitFeedback('warning', error.message);
|
||||
@@ -426,6 +435,7 @@ export async function main() {
|
||||
const loadConfigHandle = startupProfiler.start('load_cli_config');
|
||||
const config = await loadCliConfig(settings.merged, sessionId, argv, {
|
||||
projectHooks: settings.workspace.settings.hooks,
|
||||
worktreeSettings: worktreeInfo,
|
||||
});
|
||||
loadConfigHandle?.end();
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ vi.mock('./config/config.js', () => ({
|
||||
} as unknown as Config),
|
||||
parseArguments: vi.fn().mockResolvedValue({}),
|
||||
isDebugMode: vi.fn(() => false),
|
||||
getRequestedWorktreeName: vi.fn(() => undefined),
|
||||
getWorktreeArg: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock('read-package-up', () => ({
|
||||
|
||||
@@ -44,6 +44,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getDeleteSession: vi.fn(() => undefined),
|
||||
setSessionId: vi.fn(),
|
||||
getSessionId: vi.fn().mockReturnValue('mock-session-id'),
|
||||
getWorktreeSettings: vi.fn(() => undefined),
|
||||
getContentGeneratorConfig: vi.fn(() => ({ authType: 'google' })),
|
||||
getAcpMode: vi.fn(() => false),
|
||||
isBrowserLaunchSuppressed: vi.fn(() => false),
|
||||
|
||||
@@ -12,24 +12,18 @@ import { waitFor } from './async.js';
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a component', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<Text>Hello World</Text>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<Text>Hello World</Text>);
|
||||
expect(lastFrame()).toBe('Hello World\n');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should support rerender', async () => {
|
||||
const { lastFrame, rerender, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, rerender, waitUntilReady, unmount } = await render(
|
||||
<Text>Hello</Text>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe('Hello\n');
|
||||
|
||||
await act(async () => {
|
||||
rerender(<Text>World</Text>);
|
||||
});
|
||||
await act(async () => rerender(<Text>World</Text>));
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe('World\n');
|
||||
unmount();
|
||||
@@ -42,10 +36,8 @@ describe('render', () => {
|
||||
return <Text>Hello</Text>;
|
||||
}
|
||||
|
||||
const { unmount, waitUntilReady } = render(<TestComponent />);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await render(<TestComponent />);
|
||||
unmount();
|
||||
|
||||
expect(cleanupMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -54,36 +46,27 @@ describe('renderHook', () => {
|
||||
it('should rerender with previous props when called without arguments', async () => {
|
||||
const useTestHook = ({ value }: { value: number }) => {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
setCount((c) => c + 1);
|
||||
}, [value]);
|
||||
useEffect(() => setCount((c) => c + 1), [value]);
|
||||
return { count, value };
|
||||
};
|
||||
|
||||
const { result, rerender, waitUntilReady, unmount } = renderHook(
|
||||
const { result, rerender, waitUntilReady, unmount } = await renderHook(
|
||||
useTestHook,
|
||||
{
|
||||
initialProps: { value: 1 },
|
||||
},
|
||||
{ initialProps: { value: 1 } },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(result.current.value).toBe(1);
|
||||
await waitFor(() => expect(result.current.count).toBe(1));
|
||||
|
||||
// Rerender with new props
|
||||
await act(async () => {
|
||||
rerender({ value: 2 });
|
||||
});
|
||||
await act(async () => rerender({ value: 2 }));
|
||||
await waitUntilReady();
|
||||
expect(result.current.value).toBe(2);
|
||||
await waitFor(() => expect(result.current.count).toBe(2));
|
||||
|
||||
// Rerender without arguments should use previous props (value: 2)
|
||||
// This would previously crash or pass undefined if not fixed
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
await act(async () => rerender());
|
||||
await waitUntilReady();
|
||||
expect(result.current.value).toBe(2);
|
||||
// Count should not increase because value didn't change
|
||||
@@ -98,14 +81,11 @@ describe('renderHook', () => {
|
||||
};
|
||||
|
||||
const { result, rerender, waitUntilReady, unmount } =
|
||||
renderHook(useTestHook);
|
||||
await waitUntilReady();
|
||||
await renderHook(useTestHook);
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
await act(async () => rerender());
|
||||
await waitUntilReady();
|
||||
expect(result.current.count).toBe(0);
|
||||
unmount();
|
||||
@@ -113,19 +93,14 @@ describe('renderHook', () => {
|
||||
|
||||
it('should update props if undefined is passed explicitly', async () => {
|
||||
const useTestHook = (val: string | undefined) => val;
|
||||
const { result, rerender, waitUntilReady, unmount } = renderHook(
|
||||
const { result, rerender, waitUntilReady, unmount } = await renderHook(
|
||||
useTestHook,
|
||||
{
|
||||
initialProps: 'initial' as string | undefined,
|
||||
},
|
||||
{ initialProps: 'initial' },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(result.current).toBe('initial');
|
||||
|
||||
await act(async () => {
|
||||
rerender(undefined);
|
||||
});
|
||||
await act(async () => rerender(undefined));
|
||||
await waitUntilReady();
|
||||
expect(result.current).toBeUndefined();
|
||||
unmount();
|
||||
|
||||
@@ -257,13 +257,9 @@ class XtermStdout extends EventEmitter {
|
||||
return currentFrame !== '';
|
||||
}
|
||||
|
||||
// If both are empty, it's a match.
|
||||
// We consider undefined lastRenderOutput as effectively empty for this check
|
||||
// to support hook testing where Ink may skip rendering completely.
|
||||
if (
|
||||
(this.lastRenderOutput === undefined || expectedFrame === '') &&
|
||||
currentFrame === ''
|
||||
) {
|
||||
// If Ink expects nothing (no new static content and no dynamic output),
|
||||
// we consider it a match because the terminal buffer will just hold the historical static content.
|
||||
if (expectedFrame === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -271,8 +267,8 @@ class XtermStdout extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If Ink expects nothing but terminal has content, or vice-versa, it's NOT a match.
|
||||
if (expectedFrame === '' || currentFrame === '') {
|
||||
// If the terminal is empty but Ink expects something, it's not a match.
|
||||
if (currentFrame === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -382,13 +378,11 @@ export type RenderInstance = {
|
||||
|
||||
const instances: InkInstance[] = [];
|
||||
|
||||
// Wrapper around ink's render that ensures act() is called and uses Xterm for output
|
||||
export const render = (
|
||||
export const render = async (
|
||||
tree: React.ReactElement,
|
||||
terminalWidth?: number,
|
||||
): Omit<
|
||||
RenderInstance,
|
||||
'capturedOverflowState' | 'capturedOverflowActions'
|
||||
): Promise<
|
||||
Omit<RenderInstance, 'capturedOverflowState' | 'capturedOverflowActions'>
|
||||
> => {
|
||||
const cols = terminalWidth ?? 100;
|
||||
// We use 1000 rows to avoid windows with incorrect snapshots if a correct
|
||||
@@ -437,6 +431,8 @@ export const render = (
|
||||
|
||||
instances.push(instance);
|
||||
|
||||
await stdout.waitUntilReady();
|
||||
|
||||
return {
|
||||
rerender: (newTree: React.ReactElement) => {
|
||||
act(() => {
|
||||
@@ -751,7 +747,10 @@ export const renderWithProviders = async (
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
const renderResult = render(wrapWithProviders(component), terminalWidth);
|
||||
const renderResult = await render(
|
||||
wrapWithProviders(component),
|
||||
terminalWidth,
|
||||
);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
@@ -765,19 +764,19 @@ export const renderWithProviders = async (
|
||||
};
|
||||
};
|
||||
|
||||
export function renderHook<Result, Props>(
|
||||
export async function renderHook<Result, Props>(
|
||||
renderCallback: (props: Props) => Result,
|
||||
options?: {
|
||||
initialProps?: Props;
|
||||
wrapper?: React.ComponentType<{ children: React.ReactNode }>;
|
||||
},
|
||||
): {
|
||||
): Promise<{
|
||||
result: { current: Result };
|
||||
rerender: (props?: Props) => void;
|
||||
unmount: () => void;
|
||||
waitUntilReady: () => Promise<void>;
|
||||
generateSvg: () => string;
|
||||
} {
|
||||
}> {
|
||||
const result = { current: undefined as unknown as Result };
|
||||
|
||||
let currentProps = options?.initialProps as Props;
|
||||
@@ -800,17 +799,15 @@ export function renderHook<Result, Props>(
|
||||
let waitUntilReady: () => Promise<void> = async () => {};
|
||||
let generateSvg: () => string = () => '';
|
||||
|
||||
act(() => {
|
||||
const renderResult = render(
|
||||
<Wrapper>
|
||||
<TestComponent renderCallback={renderCallback} props={currentProps} />
|
||||
</Wrapper>,
|
||||
);
|
||||
inkRerender = renderResult.rerender;
|
||||
unmount = renderResult.unmount;
|
||||
waitUntilReady = renderResult.waitUntilReady;
|
||||
generateSvg = renderResult.generateSvg;
|
||||
});
|
||||
const renderResult = await render(
|
||||
<Wrapper>
|
||||
<TestComponent renderCallback={renderCallback} props={currentProps} />
|
||||
</Wrapper>,
|
||||
);
|
||||
inkRerender = renderResult.rerender;
|
||||
unmount = renderResult.unmount;
|
||||
waitUntilReady = renderResult.waitUntilReady;
|
||||
generateSvg = renderResult.generateSvg;
|
||||
|
||||
function rerender(props?: Props) {
|
||||
if (arguments.length > 0) {
|
||||
@@ -864,7 +861,13 @@ export async function renderHookWithProviders<Result, Props>(
|
||||
|
||||
const Wrapper = options.wrapper || (({ children }) => <>{children}</>);
|
||||
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
let renderResult: RenderInstance & {
|
||||
simulateClick: (
|
||||
col: number,
|
||||
row: number,
|
||||
button?: 0 | 1 | 2,
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
renderResult = await renderWithProviders(
|
||||
|
||||
@@ -94,14 +94,10 @@ describe('App', () => {
|
||||
};
|
||||
|
||||
it('should render main content and composer when not quitting', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Tips for getting started');
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
@@ -115,14 +111,10 @@ describe('App', () => {
|
||||
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: quittingUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: quittingUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Quitting...');
|
||||
unmount();
|
||||
@@ -136,14 +128,10 @@ describe('App', () => {
|
||||
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: quittingUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: quittingUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('HistoryItemDisplay');
|
||||
expect(lastFrame()).toContain('Quitting...');
|
||||
@@ -156,14 +144,10 @@ describe('App', () => {
|
||||
dialogsVisible: true,
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: dialogUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: dialogUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Tips for getting started');
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
@@ -183,14 +167,10 @@ describe('App', () => {
|
||||
[stateKey]: true,
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain(`Press Ctrl+${key} again to exit.`);
|
||||
unmount();
|
||||
@@ -200,14 +180,10 @@ describe('App', () => {
|
||||
it('should render ScreenReaderAppLayout when screen reader is enabled', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
@@ -219,14 +195,10 @@ describe('App', () => {
|
||||
it('should render DefaultAppLayout when screen reader is not enabled', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Tips for getting started');
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
@@ -274,15 +246,11 @@ describe('App', () => {
|
||||
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
|
||||
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: stateWithConfirmingTool,
|
||||
config: configWithExperiment,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: stateWithConfirmingTool,
|
||||
config: configWithExperiment,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('Tips for getting started');
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
@@ -295,28 +263,20 @@ describe('App', () => {
|
||||
describe('Snapshots', () => {
|
||||
it('renders default layout correctly', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders screen reader layout correctly', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -326,14 +286,10 @@ describe('App', () => {
|
||||
...mockUIState,
|
||||
dialogsVisible: true,
|
||||
} as UIState;
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: dialogUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<App />, {
|
||||
uiState: dialogUIState,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,10 +53,9 @@ describe('IdeIntegrationNudge', () => {
|
||||
});
|
||||
|
||||
it('renders correctly with default options', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<IdeIntegrationNudge {...defaultProps} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
|
||||
expect(frame).toContain('Do you want to connect VS Code to Gemini CLI?');
|
||||
@@ -72,8 +71,6 @@ describe('IdeIntegrationNudge', () => {
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// "Yes" is the first option and selected by default usually.
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
@@ -93,8 +90,6 @@ describe('IdeIntegrationNudge', () => {
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// Navigate down to "No (esc)"
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
@@ -119,8 +114,6 @@ describe('IdeIntegrationNudge', () => {
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// Navigate down to "No, don't ask again"
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
@@ -150,8 +143,6 @@ describe('IdeIntegrationNudge', () => {
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// Press Escape
|
||||
await act(async () => {
|
||||
stdin.write('\u001B');
|
||||
@@ -178,8 +169,6 @@ describe('IdeIntegrationNudge', () => {
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame();
|
||||
|
||||
expect(frame).toContain(
|
||||
|
||||
@@ -73,23 +73,21 @@ describe('ApiAuthDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with a defaultValue', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ApiAuthDialog
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
defaultValue="test-key"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(mockedUseTextBuffer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialText: 'test-key',
|
||||
@@ -113,10 +111,9 @@ describe('ApiAuthDialog', () => {
|
||||
'calls $expectedCall.name when $keyName is pressed',
|
||||
async ({ keyName, sequence, expectedCall, args }) => {
|
||||
mockBuffer.text = 'submitted-key'; // Set for the onSubmit case
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// calls[0] is the ApiAuthDialog's useKeypress (Ctrl+C handler)
|
||||
// calls[1] is the TextInput's useKeypress (typing handler)
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[1][0];
|
||||
@@ -136,24 +133,22 @@ describe('ApiAuthDialog', () => {
|
||||
);
|
||||
|
||||
it('displays an error message', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ApiAuthDialog
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
error="Invalid API Key"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Invalid API Key');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls clearApiKey and clears buffer when Ctrl+C is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// Call 0 is ApiAuthDialog (isActive: true)
|
||||
// Call 1 is TextInput (isActive: true, priority: true)
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
@@ -143,10 +143,9 @@ describe('AuthDialog', () => {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
vi.stubEnv(key, value as string);
|
||||
}
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
for (const item of shouldContain) {
|
||||
expect(items).toContainEqual(item);
|
||||
@@ -161,10 +160,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('filters auth types when enforcedType is set', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].value).toBe(AuthType.USE_GEMINI);
|
||||
@@ -173,10 +169,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets initial index to 0 when enforcedType is set', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(initialIndex).toBe(0);
|
||||
unmount();
|
||||
@@ -213,10 +206,7 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
])('selects initial auth type $desc', async ({ setup, expected }) => {
|
||||
setup();
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(items[initialIndex].value).toBe(expected);
|
||||
unmount();
|
||||
@@ -226,10 +216,7 @@ describe('AuthDialog', () => {
|
||||
describe('handleAuthSelect', () => {
|
||||
it('calls onAuthError if validation fails', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue('Invalid method');
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -245,10 +232,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
||||
@@ -261,10 +245,7 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('sets auth context with empty object for other auth types', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -278,10 +259,7 @@ describe('AuthDialog', () => {
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -297,10 +275,7 @@ describe('AuthDialog', () => {
|
||||
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
|
||||
// props.settings.merged.security.auth.selectedType is undefined here
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -316,10 +291,7 @@ describe('AuthDialog', () => {
|
||||
// process.env['GEMINI_API_KEY'] is not set
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -337,10 +309,7 @@ describe('AuthDialog', () => {
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -360,10 +329,7 @@ describe('AuthDialog', () => {
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -383,10 +349,9 @@ describe('AuthDialog', () => {
|
||||
|
||||
it('displays authError when provided', async () => {
|
||||
props.authError = 'Something went wrong';
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Something went wrong');
|
||||
unmount();
|
||||
});
|
||||
@@ -429,10 +394,7 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
])('$desc', async ({ setup, expectations }) => {
|
||||
setup();
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<AuthDialog {...props} />);
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
keypressHandler({ name: 'escape' });
|
||||
expectations(props);
|
||||
@@ -442,30 +404,27 @@ describe('AuthDialog', () => {
|
||||
|
||||
describe('Snapshots', () => {
|
||||
it('renders correctly with default props', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with auth error', async () => {
|
||||
props.authError = 'Something went wrong';
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with enforced auth type', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -55,20 +55,18 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('renders initial state with spinner', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('[Spinner] Waiting for authentication...');
|
||||
expect(lastFrame()).toContain('Press Esc or Ctrl+C to cancel');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onTimeout when ESC is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
@@ -84,10 +82,9 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('calls onTimeout when Ctrl+C is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
|
||||
|
||||
await act(async () => {
|
||||
@@ -100,10 +97,9 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('calls onTimeout and shows timeout message after 3 minutes', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = await render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(180000);
|
||||
@@ -116,10 +112,7 @@ describe('AuthInProgress', () => {
|
||||
});
|
||||
|
||||
it('clears timer on unmount', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
|
||||
await act(async () => {
|
||||
unmount();
|
||||
|
||||
@@ -73,14 +73,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('renders the suspension message from accountSuspensionInfo', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Account Suspended');
|
||||
expect(frame).toContain('violation of Terms of Service');
|
||||
@@ -89,14 +88,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('renders menu options with appeal link text from response', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(3);
|
||||
expect(items[0].label).toBe('Appeal Here');
|
||||
@@ -109,14 +107,13 @@ describe('BannedAccountDialog', () => {
|
||||
const infoWithoutUrl: AccountSuspensionInfo = {
|
||||
message: 'Account suspended.',
|
||||
};
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={infoWithoutUrl}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0].label).toBe('Change authentication');
|
||||
@@ -129,28 +126,26 @@ describe('BannedAccountDialog', () => {
|
||||
message: 'Account suspended.',
|
||||
appealUrl: 'https://example.com/appeal',
|
||||
};
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={infoWithoutLinkText}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
expect(items[0].label).toBe('Open the Google Form');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('opens browser when appeal option is selected', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await onSelect('open_form');
|
||||
expect(mockedOpenBrowser).toHaveBeenCalledWith(
|
||||
@@ -162,14 +157,13 @@ describe('BannedAccountDialog', () => {
|
||||
|
||||
it('shows URL when browser cannot be launched', async () => {
|
||||
mockedShouldLaunchBrowser.mockReturnValue(false);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
onSelect('open_form');
|
||||
await waitFor(() => {
|
||||
@@ -180,14 +174,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('calls onExit when "Exit" is selected', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await onSelect('exit');
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||
@@ -196,14 +189,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('calls onChangeAuth when "Change authentication" is selected', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
onSelect('change_auth');
|
||||
expect(onChangeAuth).toHaveBeenCalled();
|
||||
@@ -212,14 +204,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('exits on escape key', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
const result = keypressHandler({ name: 'escape' });
|
||||
expect(result).toBe(true);
|
||||
@@ -227,14 +218,13 @@ describe('BannedAccountDialog', () => {
|
||||
});
|
||||
|
||||
it('renders snapshot correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<BannedAccountDialog
|
||||
accountSuspensionInfo={DEFAULT_SUSPENSION_INFO}
|
||||
onExit={onExit}
|
||||
onChangeAuth={onChangeAuth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -45,25 +45,23 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onDismiss when escape is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
@@ -83,13 +81,12 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
|
||||
@@ -4,15 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
vi,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useAuthCommand, validateAuthMethodWithSettings } from './useAuth.js';
|
||||
import {
|
||||
@@ -22,7 +15,6 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { AuthState } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
|
||||
// Mock dependencies
|
||||
const mockLoadApiKey = vi.fn();
|
||||
@@ -142,171 +134,202 @@ describe('useAuth', () => {
|
||||
},
|
||||
}) as LoadedSettings;
|
||||
|
||||
let deferredRefreshAuth: {
|
||||
resolve: () => void;
|
||||
reject: (e: Error) => void;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(mockConfig.refreshAuth).mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
deferredRefreshAuth = { resolve, reject };
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize with Unauthenticated state', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
// Because we defer refreshAuth, the initial state is safely caught here
|
||||
expect(result.current.authState).toBe(AuthState.Unauthenticated);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
});
|
||||
|
||||
it('should set error if no auth type is selected and no env key', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(undefined), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toBe(
|
||||
'No authentication method selected.',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
// This happens synchronously, no deferred promise
|
||||
expect(result.current.authError).toBe(
|
||||
'No authentication method selected.',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should set error if no auth type is selected but env key exists', async () => {
|
||||
process.env['GEMINI_API_KEY'] = 'env-key';
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(undefined), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toContain(
|
||||
'Existing API key detected (GEMINI_API_KEY)',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
expect(result.current.authError).toContain(
|
||||
'Existing API key detected (GEMINI_API_KEY)',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should transition to AwaitingApiKeyInput if USE_GEMINI and no key found', async () => {
|
||||
mockLoadApiKey.mockResolvedValue(null);
|
||||
const { result } = renderHook(() =>
|
||||
let deferredLoadKey: { resolve: (k: string | null) => void };
|
||||
mockLoadApiKey.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
deferredLoadKey = { resolve };
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authState).toBe(AuthState.AwaitingApiKeyInput);
|
||||
await act(async () => {
|
||||
deferredLoadKey.resolve(null);
|
||||
});
|
||||
|
||||
expect(result.current.authState).toBe(AuthState.AwaitingApiKeyInput);
|
||||
});
|
||||
|
||||
it('should authenticate if USE_GEMINI and key is found', async () => {
|
||||
mockLoadApiKey.mockResolvedValue('stored-key');
|
||||
const { result } = renderHook(() =>
|
||||
let deferredLoadKey: { resolve: (k: string | null) => void };
|
||||
mockLoadApiKey.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
deferredLoadKey = { resolve };
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('stored-key');
|
||||
await act(async () => {
|
||||
deferredLoadKey.resolve('stored-key');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('stored-key');
|
||||
});
|
||||
|
||||
it('should authenticate if USE_GEMINI and env key is found', async () => {
|
||||
mockLoadApiKey.mockResolvedValue(null);
|
||||
process.env['GEMINI_API_KEY'] = 'env-key';
|
||||
const { result } = renderHook(() =>
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
});
|
||||
|
||||
it('should prioritize env key over stored key when both are present', async () => {
|
||||
mockLoadApiKey.mockResolvedValue('stored-key');
|
||||
process.env['GEMINI_API_KEY'] = 'env-key';
|
||||
const { result } = renderHook(() =>
|
||||
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.USE_GEMINI), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
// The environment key should take precedence
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_GEMINI);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.apiKeyDefaultValue).toBe('env-key');
|
||||
});
|
||||
|
||||
it('should set error if validation fails', async () => {
|
||||
mockValidateAuthMethod.mockReturnValue('Validation Failed');
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toBe('Validation Failed');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
expect(result.current.authError).toBe('Validation Failed');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should set error if GEMINI_DEFAULT_AUTH_TYPE is invalid', async () => {
|
||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = 'INVALID_TYPE';
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toContain(
|
||||
'Invalid value for GEMINI_DEFAULT_AUTH_TYPE',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
expect(result.current.authError).toContain(
|
||||
'Invalid value for GEMINI_DEFAULT_AUTH_TYPE',
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should authenticate successfully for valid auth type', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.authError).toBeNull();
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.resolve();
|
||||
});
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
);
|
||||
expect(result.current.authState).toBe(AuthState.Authenticated);
|
||||
expect(result.current.authError).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle refreshAuth failure', async () => {
|
||||
(mockConfig.refreshAuth as Mock).mockRejectedValue(
|
||||
new Error('Auth Failed'),
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toContain('Failed to sign in');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.reject(new Error('Auth Failed'));
|
||||
});
|
||||
|
||||
expect(result.current.authError).toContain('Failed to sign in');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
|
||||
it('should handle ProjectIdRequiredError without "Failed to login" prefix', async () => {
|
||||
const projectIdError = new ProjectIdRequiredError();
|
||||
(mockConfig.refreshAuth as Mock).mockRejectedValue(projectIdError);
|
||||
const { result } = renderHook(() =>
|
||||
const { result } = await renderHook(() =>
|
||||
useAuthCommand(createSettings(AuthType.LOGIN_WITH_GOOGLE), mockConfig),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.authError).toBe(
|
||||
'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca',
|
||||
);
|
||||
expect(result.current.authError).not.toContain('Failed to login');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
await act(async () => {
|
||||
deferredRefreshAuth.reject(projectIdError);
|
||||
});
|
||||
|
||||
expect(result.current.authError).toBe(
|
||||
'This account requires setting the GOOGLE_CLOUD_PROJECT or GOOGLE_CLOUD_PROJECT_ID env var. See https://goo.gle/gemini-cli-auth-docs#workspace-gca',
|
||||
);
|
||||
expect(result.current.authError).not.toContain('Failed to login');
|
||||
expect(result.current.authState).toBe(AuthState.Updating);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -710,10 +710,14 @@ describe('extensionsCommand', () => {
|
||||
size: 100,
|
||||
} as Stats);
|
||||
await linkAction!(mockContext, packageName);
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith({
|
||||
source: packageName,
|
||||
type: 'link',
|
||||
});
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith(
|
||||
{
|
||||
source: packageName,
|
||||
type: 'link',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.INFO,
|
||||
text: `Linking extension from "${packageName}"...`,
|
||||
@@ -733,10 +737,14 @@ describe('extensionsCommand', () => {
|
||||
} as Stats);
|
||||
|
||||
await linkAction!(mockContext, packageName);
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith({
|
||||
source: packageName,
|
||||
type: 'link',
|
||||
});
|
||||
expect(mockInstallExtension).toHaveBeenCalledWith(
|
||||
{
|
||||
source: packageName,
|
||||
type: 'link',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||
type: MessageType.ERROR,
|
||||
text: `Failed to link extension from "${packageName}": ${errorMessage}`,
|
||||
|
||||
@@ -286,6 +286,11 @@ async function exploreAction(
|
||||
await installAction(context, extension.url, requestConsentOverride);
|
||||
context.ui.removeComponent();
|
||||
},
|
||||
onLink: async (extension, requestConsentOverride) => {
|
||||
debugLogger.log(`Linking extension: ${extension.extensionName}`);
|
||||
await linkAction(context, extension.url, requestConsentOverride);
|
||||
context.ui.removeComponent();
|
||||
},
|
||||
onClose: () => context.ui.removeComponent(),
|
||||
extensionManager,
|
||||
}),
|
||||
@@ -533,7 +538,11 @@ async function installAction(
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAction(context: CommandContext, args: string) {
|
||||
async function linkAction(
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
requestConsentOverride?: (consent: string) => Promise<boolean>,
|
||||
) {
|
||||
const extensionLoader =
|
||||
context.services.agentContext?.config.getExtensionLoader();
|
||||
if (!(extensionLoader instanceof ExtensionManager)) {
|
||||
@@ -582,8 +591,11 @@ async function linkAction(context: CommandContext, args: string) {
|
||||
source: sourceFilepath,
|
||||
type: 'link',
|
||||
};
|
||||
const extension =
|
||||
await extensionLoader.installOrUpdateExtension(installMetadata);
|
||||
const extension = await extensionLoader.installOrUpdateExtension(
|
||||
installMetadata,
|
||||
undefined,
|
||||
requestConsentOverride,
|
||||
);
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: `Extension "${extension.name}" linked successfully.`,
|
||||
|
||||
@@ -25,10 +25,9 @@ describe('AboutBox', () => {
|
||||
};
|
||||
|
||||
it('renders with required props', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AboutBox {...defaultProps} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('About Gemini CLI');
|
||||
expect(output).toContain('1.0.0');
|
||||
@@ -46,10 +45,9 @@ describe('AboutBox', () => {
|
||||
['tier', 'Enterprise', 'Tier'],
|
||||
])('renders optional prop %s', async (prop, value, label) => {
|
||||
const props = { ...defaultProps, [prop]: value };
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AboutBox {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(label);
|
||||
expect(output).toContain(value);
|
||||
@@ -58,10 +56,9 @@ describe('AboutBox', () => {
|
||||
|
||||
it('renders Auth Method with email when userEmail is provided', async () => {
|
||||
const props = { ...defaultProps, userEmail: 'test@example.com' };
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AboutBox {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Signed in with Google (test@example.com)');
|
||||
unmount();
|
||||
@@ -69,10 +66,9 @@ describe('AboutBox', () => {
|
||||
|
||||
it('renders Auth Method correctly when not oauth', async () => {
|
||||
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AboutBox {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('api-key');
|
||||
unmount();
|
||||
|
||||
@@ -17,15 +17,14 @@ describe('AdminSettingsChangedDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AdminSettingsChangedDialog />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('restarts on "r" key press', async () => {
|
||||
const { stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { stdin } = await renderWithProviders(
|
||||
<AdminSettingsChangedDialog />,
|
||||
{
|
||||
uiActions: {
|
||||
@@ -33,7 +32,6 @@ describe('AdminSettingsChangedDialog', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
stdin.write('r');
|
||||
@@ -43,7 +41,7 @@ describe('AdminSettingsChangedDialog', () => {
|
||||
});
|
||||
|
||||
it.each(['r', 'R'])('restarts on "%s" key press', async (key) => {
|
||||
const { stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { stdin } = await renderWithProviders(
|
||||
<AdminSettingsChangedDialog />,
|
||||
{
|
||||
uiActions: {
|
||||
@@ -51,7 +49,6 @@ describe('AdminSettingsChangedDialog', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
stdin.write(key);
|
||||
|
||||
@@ -126,7 +126,6 @@ describe('AgentConfigDialog', () => {
|
||||
/>,
|
||||
{ settings, uiState: { mainAreaWidth: 100 } },
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
|
||||
it('renders with active and pending tool messages', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -118,14 +118,13 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_history_and_pending');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with empty history and no pending items', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -135,14 +134,13 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('empty');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with history but no pending items', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -152,14 +150,13 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_history_no_pending');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with pending items but no history', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -169,7 +166,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_pending_no_history');
|
||||
unmount();
|
||||
});
|
||||
@@ -195,7 +191,7 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
],
|
||||
},
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -205,7 +201,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Action Required (was prompted):');
|
||||
expect(output).toContain('confirming_tool');
|
||||
@@ -220,7 +215,7 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
{ id: 1, type: 'user', text: 'Hello Gemini' },
|
||||
{ id: 2, type: 'gemini', text: 'Hello User!' },
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -230,7 +225,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_user_gemini_messages');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -29,10 +29,9 @@ describe('<AnsiOutputText />', () => {
|
||||
createAnsiToken({ text: 'world!' }),
|
||||
],
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame().trim()).toBe('Hello, world!');
|
||||
unmount();
|
||||
});
|
||||
@@ -47,10 +46,9 @@ describe('<AnsiOutputText />', () => {
|
||||
{ style: { inverse: true }, text: 'Inverse' },
|
||||
])('correctly applies style $text', async ({ style, text }) => {
|
||||
const data: AnsiOutput = [[createAnsiToken({ text, ...style })]];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame().trim()).toBe(text);
|
||||
unmount();
|
||||
});
|
||||
@@ -61,10 +59,9 @@ describe('<AnsiOutputText />', () => {
|
||||
{ color: { fg: '#00ff00', bg: '#ff00ff' }, text: 'Green FG Magenta BG' },
|
||||
])('correctly applies color $text', async ({ color, text }) => {
|
||||
const data: AnsiOutput = [[createAnsiToken({ text, ...color })]];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame().trim()).toBe(text);
|
||||
unmount();
|
||||
});
|
||||
@@ -76,10 +73,9 @@ describe('<AnsiOutputText />', () => {
|
||||
[createAnsiToken({ text: 'Third line' })],
|
||||
[createAnsiToken({ text: '' })],
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
const lines = output.split('\n');
|
||||
@@ -96,10 +92,9 @@ describe('<AnsiOutputText />', () => {
|
||||
[createAnsiToken({ text: 'Line 3' })],
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={data} availableTerminalHeight={2} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).not.toContain('Line 2');
|
||||
@@ -115,10 +110,9 @@ describe('<AnsiOutputText />', () => {
|
||||
[createAnsiToken({ text: 'Line 3' })],
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={data} maxLines={2} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).not.toContain('Line 2');
|
||||
@@ -135,7 +129,7 @@ describe('<AnsiOutputText />', () => {
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
// availableTerminalHeight=3, maxLines=2 => show 2 lines
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText
|
||||
data={data}
|
||||
availableTerminalHeight={3}
|
||||
@@ -143,7 +137,6 @@ describe('<AnsiOutputText />', () => {
|
||||
width={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
@@ -156,10 +149,9 @@ describe('<AnsiOutputText />', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
largeData.push([createAnsiToken({ text: `Line ${i}` })]);
|
||||
}
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText data={largeData} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// We are just checking that it renders something without crashing.
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
|
||||
@@ -27,13 +27,12 @@ describe('<AppHeader />', () => {
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -50,13 +49,12 @@ describe('<AppHeader />', () => {
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('There are capacity issues');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -72,13 +70,12 @@ describe('<AppHeader />', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -103,13 +100,12 @@ describe('<AppHeader />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -129,13 +125,12 @@ describe('<AppHeader />', () => {
|
||||
// and interfering with the expected persistentState.set call.
|
||||
persistentStateMock.setData({ tipsShown: 10 });
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
'defaultBannerShownCount',
|
||||
@@ -159,13 +154,12 @@ describe('<AppHeader />', () => {
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('First line\\nSecond line');
|
||||
unmount();
|
||||
@@ -183,13 +177,12 @@ describe('<AppHeader />', () => {
|
||||
|
||||
persistentStateMock.setData({ tipsShown: 5 });
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Tips');
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith('tipsShown', 6);
|
||||
@@ -206,13 +199,12 @@ describe('<AppHeader />', () => {
|
||||
|
||||
persistentStateMock.setData({ tipsShown: 10 });
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Tips');
|
||||
unmount();
|
||||
@@ -234,7 +226,6 @@ describe('<AppHeader />', () => {
|
||||
const session1 = await renderWithProviders(<AppHeader version="1.0.0" />, {
|
||||
uiState,
|
||||
});
|
||||
await session1.waitUntilReady();
|
||||
|
||||
expect(session1.lastFrame()).toContain('Tips');
|
||||
expect(persistentStateMock.get('tipsShown')).toBe(10);
|
||||
@@ -245,7 +236,6 @@ describe('<AppHeader />', () => {
|
||||
<AppHeader version="1.0.0" />,
|
||||
{},
|
||||
);
|
||||
await session2.waitUntilReady();
|
||||
|
||||
expect(session2.lastFrame()).not.toContain('Tips');
|
||||
session2.unmount();
|
||||
|
||||
@@ -11,56 +11,50 @@ import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
|
||||
describe('ApprovalModeIndicator', () => {
|
||||
it('renders correctly for AUTO_EDIT mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for AUTO_EDIT mode with plan enabled', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.AUTO_EDIT}
|
||||
allowPlanMode={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for PLAN mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for YOLO mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode with plan enabled', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.DEFAULT}
|
||||
allowPlanMode={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('AskUserDialog', () => {
|
||||
];
|
||||
|
||||
it('renders question and options', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -58,7 +58,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -397,7 +396,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -407,12 +406,11 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('hides progress header for single question', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -422,12 +420,11 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows keyboard hints', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -437,7 +434,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -471,7 +467,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which testing framework?');
|
||||
|
||||
writeKey(stdin, '\x1b[C'); // Right arrow
|
||||
@@ -582,7 +577,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -592,7 +587,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -736,7 +730,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -746,7 +740,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -759,7 +752,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -769,7 +762,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -820,7 +812,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -830,7 +822,6 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('renders the output of the active shell', async () => {
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -158,7 +158,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -166,7 +165,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('renders tabs for multiple shells', async () => {
|
||||
const width = 100;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -179,7 +178,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -187,7 +185,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('highlights the focused state', async () => {
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -200,7 +198,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -208,7 +205,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('resizes the PTY on mount and when dimensions change', async () => {
|
||||
const width = 80;
|
||||
const { rerender, waitUntilReady, unmount } = render(
|
||||
const { rerender, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -221,7 +218,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(ShellExecutionService.resizePty).toHaveBeenCalledWith(
|
||||
shell1.pid,
|
||||
@@ -241,7 +237,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(ShellExecutionService.resizePty).toHaveBeenCalledWith(
|
||||
shell1.pid,
|
||||
@@ -253,7 +248,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('renders the process list when isListOpenProp is true', async () => {
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -266,7 +261,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -274,7 +268,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('selects the current process and closes the list when Ctrl+L is pressed in list view', async () => {
|
||||
const width = 80;
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -287,19 +281,16 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Simulate down arrow to select the second process (handled by RadioButtonSelect)
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Simulate Ctrl+L (handled by BackgroundShellDisplay)
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'l', ctrl: true });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(mockSetActiveBackgroundShellPid).toHaveBeenCalledWith(shell2.pid);
|
||||
expect(mockSetIsBackgroundShellListOpen).toHaveBeenCalledWith(false);
|
||||
@@ -308,7 +299,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('kills the highlighted process when Ctrl+K is pressed in list view', async () => {
|
||||
const width = 80;
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -321,7 +312,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initial state: shell1 (active) is highlighted
|
||||
|
||||
@@ -329,13 +319,11 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Press Ctrl+K
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell2.pid);
|
||||
unmount();
|
||||
@@ -343,7 +331,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
|
||||
it('kills the active process when Ctrl+K is pressed in output view', async () => {
|
||||
const width = 80;
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -356,12 +344,10 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell1.pid);
|
||||
unmount();
|
||||
@@ -370,7 +356,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
it('scrolls to active shell when list opens', async () => {
|
||||
// shell2 is active
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -383,7 +369,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -402,7 +387,7 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
mockShells.set(exitedShell.pid, exitedShell);
|
||||
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
@@ -415,7 +400,6 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
|
||||
@@ -18,10 +18,9 @@ describe('<Checklist />', () => {
|
||||
];
|
||||
|
||||
it('renders nothing when list is empty', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Checklist title="Test List" items={[]} isExpanded={true} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
});
|
||||
|
||||
@@ -30,15 +29,14 @@ describe('<Checklist />', () => {
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'cancelled', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Checklist title="Test List" items={inactiveItems} isExpanded={false} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
});
|
||||
|
||||
it('renders summary view correctly (collapsed)', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
@@ -46,12 +44,11 @@ describe('<Checklist />', () => {
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expanded view correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
@@ -59,7 +56,6 @@ describe('<Checklist />', () => {
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -68,10 +64,9 @@ describe('<Checklist />', () => {
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'pending', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Checklist title="Test List" items={pendingItems} isExpanded={false} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,8 +17,7 @@ describe('<ChecklistItem />', () => {
|
||||
{ status: 'cancelled', label: 'Skipped this' },
|
||||
{ status: 'blocked', label: 'Blocked this' },
|
||||
] as ChecklistItemData[])('renders %s item correctly', async (item) => {
|
||||
const { lastFrame, waitUntilReady } = render(<ChecklistItem item={item} />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame } = await render(<ChecklistItem item={item} />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -28,12 +27,11 @@ describe('<ChecklistItem />', () => {
|
||||
label:
|
||||
'This is a very long text that should be truncated because the wrap prop is set to truncate',
|
||||
};
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} wrap="truncate" />
|
||||
</Box>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -43,12 +41,11 @@ describe('<ChecklistItem />', () => {
|
||||
label:
|
||||
'This is a very long text that should wrap because the default behavior is wrapping',
|
||||
};
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
const { lastFrame } = await render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} />
|
||||
</Box>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,7 @@ describe('<CliSpinner />', () => {
|
||||
|
||||
it('should increment debugNumAnimatedComponents on mount and decrement on unmount', async () => {
|
||||
expect(debugState.debugNumAnimatedComponents).toBe(0);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<CliSpinner />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<CliSpinner />);
|
||||
expect(debugState.debugNumAnimatedComponents).toBe(1);
|
||||
unmount();
|
||||
expect(debugState.debugNumAnimatedComponents).toBe(0);
|
||||
@@ -28,11 +25,9 @@ describe('<CliSpinner />', () => {
|
||||
|
||||
it('should not render when showSpinner is false', async () => {
|
||||
const settings = createMockSettings({ ui: { showSpinner: false } });
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<CliSpinner />,
|
||||
{ settings },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<CliSpinner />, {
|
||||
settings,
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -96,10 +96,9 @@ describe('ColorsDisplay', () => {
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const mockTheme = themeManager.getActiveTheme();
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ColorsDisplay activeTheme={mockTheme} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
// Check for title and description
|
||||
|
||||
@@ -251,7 +251,7 @@ const renderComposer = async (
|
||||
config = createMockConfig(),
|
||||
uiActions = createMockUIActions(),
|
||||
) => {
|
||||
const result = render(
|
||||
const result = await render(
|
||||
<ConfigContext.Provider value={config as unknown as Config}>
|
||||
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
@@ -262,7 +262,6 @@ const renderComposer = async (
|
||||
</SettingsContext.Provider>
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
|
||||
// Wait for shortcuts hint debounce if using fake timers
|
||||
if (vi.isFakeTimers()) {
|
||||
|
||||
@@ -43,10 +43,7 @@ describe('ConfigInitDisplay', () => {
|
||||
});
|
||||
|
||||
it('renders initial state', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<ConfigInitDisplay />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame } = await renderWithProviders(<ConfigInitDisplay />);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -33,14 +33,13 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('renders a string prompt with MarkdownDisplay', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(MockedMarkdownDisplay).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -55,14 +54,13 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('renders a ReactNode prompt directly', async () => {
|
||||
const prompt = <Text>Are you sure?</Text>;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(MockedMarkdownDisplay).not.toHaveBeenCalled();
|
||||
expect(lastFrame()).toContain('Are you sure?');
|
||||
@@ -71,14 +69,13 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('calls onConfirm with true when "Yes" is selected', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
await act(async () => {
|
||||
@@ -92,14 +89,13 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('calls onConfirm with false when "No" is selected', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
await act(async () => {
|
||||
@@ -113,14 +109,13 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('passes correct items to RadioButtonSelect', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { unmount } = await render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(MockedRadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -10,10 +10,9 @@ import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ConsoleSummaryDisplay', () => {
|
||||
it('renders nothing when errorCount is 0', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ConsoleSummaryDisplay errorCount={0} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -22,10 +21,9 @@ describe('ConsoleSummaryDisplay', () => {
|
||||
[1, '1 error'],
|
||||
[5, '5 errors'],
|
||||
])('renders correct message for %i errors', async (count, expectedText) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ConsoleSummaryDisplay errorCount={count} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(expectedText);
|
||||
expect(output).toContain('✖');
|
||||
|
||||
@@ -26,8 +26,7 @@ const renderWithWidth = async (
|
||||
props: React.ComponentProps<typeof ContextSummaryDisplay>,
|
||||
) => {
|
||||
useTerminalSizeMock.mockReturnValue({ columns: width, rows: 24 });
|
||||
const result = render(<ContextSummaryDisplay {...props} />);
|
||||
await result.waitUntilReady();
|
||||
const result = await render(<ContextSummaryDisplay {...props} />);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,35 +19,33 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
|
||||
describe('ContextUsageDisplay', () => {
|
||||
it('renders correct percentage used', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={5000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('50% used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly when usage is 0%', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={0}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('0% used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders abbreviated label when terminal width is small', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={2000}
|
||||
model="gemini-pro"
|
||||
@@ -55,7 +53,6 @@ describe('ContextUsageDisplay', () => {
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('20%');
|
||||
expect(output).not.toContain('context used');
|
||||
@@ -63,28 +60,26 @@ describe('ContextUsageDisplay', () => {
|
||||
});
|
||||
|
||||
it('renders 80% correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={8000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('80% used');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders 100% when full', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={10000}
|
||||
model="gemini-pro"
|
||||
terminalWidth={120}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('100% used');
|
||||
unmount();
|
||||
|
||||
@@ -22,8 +22,7 @@ describe('CopyModeWarning', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
copyModeEnabled: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<CopyModeWarning />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<CopyModeWarning />);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -32,8 +31,7 @@ describe('CopyModeWarning', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
copyModeEnabled: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<CopyModeWarning />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<CopyModeWarning />);
|
||||
expect(lastFrame()).toContain('In Copy Mode');
|
||||
expect(lastFrame()).toContain('Use Page Up/Down to scroll');
|
||||
expect(lastFrame()).toContain('Press Ctrl+S or any other key to exit');
|
||||
|
||||
@@ -242,8 +242,7 @@ describe('DebugProfiler Component', () => {
|
||||
showDebugProfiler: false,
|
||||
constrainHeight: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<DebugProfiler />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<DebugProfiler />);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -257,8 +256,7 @@ describe('DebugProfiler Component', () => {
|
||||
profiler.totalIdleFrames = 5;
|
||||
profiler.totalFlickerFrames = 2;
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<DebugProfiler />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<DebugProfiler />);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Renders: 10 (total)');
|
||||
@@ -275,8 +273,7 @@ describe('DebugProfiler Component', () => {
|
||||
|
||||
const reportActionSpy = vi.spyOn(profiler, 'reportAction');
|
||||
|
||||
const { waitUntilReady, unmount } = render(<DebugProfiler />);
|
||||
await waitUntilReady();
|
||||
const { waitUntilReady, unmount } = await render(<DebugProfiler />);
|
||||
|
||||
await act(async () => {
|
||||
coreEvents.emitModelChanged('new-model');
|
||||
@@ -295,8 +292,7 @@ describe('DebugProfiler Component', () => {
|
||||
|
||||
const reportActionSpy = vi.spyOn(profiler, 'reportAction');
|
||||
|
||||
const { waitUntilReady, unmount } = render(<DebugProfiler />);
|
||||
await waitUntilReady();
|
||||
const { waitUntilReady, unmount } = await render(<DebugProfiler />);
|
||||
|
||||
await act(async () => {
|
||||
appEvents.emit(AppEvent.SelectionWarning);
|
||||
|
||||
@@ -41,13 +41,12 @@ describe('DetailedMessagesDisplay', () => {
|
||||
});
|
||||
});
|
||||
it('renders nothing when messages are empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -64,13 +63,12 @@ describe('DetailedMessagesDisplay', () => {
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
@@ -86,13 +84,12 @@ describe('DetailedMessagesDisplay', () => {
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('(F12 to close)');
|
||||
unmount();
|
||||
});
|
||||
@@ -106,13 +103,12 @@ describe('DetailedMessagesDisplay', () => {
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={20} width={80} hasFocus={true} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('(F12 to close)');
|
||||
unmount();
|
||||
});
|
||||
@@ -126,13 +122,12 @@ describe('DetailedMessagesDisplay', () => {
|
||||
clearConsoleMessages: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DetailedMessagesDisplay maxHeight={10} width={80} hasFocus={false} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
@@ -104,11 +104,10 @@ describe('DialogManager', () => {
|
||||
};
|
||||
|
||||
it('renders nothing by default', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DialogManager {...defaultProps} />,
|
||||
{ uiState: baseUiState as Partial<UIState> as UIState },
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -197,7 +196,7 @@ describe('DialogManager', () => {
|
||||
it.each(testCases)(
|
||||
'renders %s when state is %o',
|
||||
async (uiStateOverride, expectedComponent) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DialogManager {...defaultProps} />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -206,7 +205,6 @@ describe('DialogManager', () => {
|
||||
} as Partial<UIState> as UIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(expectedComponent);
|
||||
unmount();
|
||||
},
|
||||
|
||||
@@ -55,27 +55,25 @@ describe('EditorSettingsDialog', () => {
|
||||
renderWithProviders(ui);
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProvider(
|
||||
const { lastFrame } = await renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={vi.fn()}
|
||||
settings={mockSettings}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('calls onSelect when an editor is selected', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const { lastFrame, waitUntilReady } = await renderWithProvider(
|
||||
const { lastFrame } = await renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={onSelect}
|
||||
settings={mockSettings}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('VS Code');
|
||||
});
|
||||
@@ -88,7 +86,6 @@ describe('EditorSettingsDialog', () => {
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initial focus on editor
|
||||
expect(lastFrame()).toContain('> Select Editor');
|
||||
@@ -134,7 +131,6 @@ describe('EditorSettingsDialog', () => {
|
||||
onExit={onExit}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B'); // Escape
|
||||
@@ -162,14 +158,13 @@ describe('EditorSettingsDialog', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const { lastFrame, waitUntilReady } = await renderWithProvider(
|
||||
const { lastFrame } = await renderWithProvider(
|
||||
<EditorSettingsDialog
|
||||
onSelect={vi.fn()}
|
||||
settings={settingsWithOtherScope}
|
||||
onExit={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame() || '';
|
||||
if (!frame.includes('(Also modified')) {
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('EmptyWalletDialog', () => {
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should match snapshot with fallback available', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-3-flash-preview"
|
||||
@@ -38,33 +38,30 @@ describe('EmptyWalletDialog', () => {
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should match snapshot without fallback', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display the model name and usage limit message', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('gemini-2.5-pro');
|
||||
@@ -73,13 +70,12 @@ describe('EmptyWalletDialog', () => {
|
||||
});
|
||||
|
||||
it('should display purchase prompt and credits update notice', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('purchase more AI Credits');
|
||||
@@ -90,14 +86,13 @@ describe('EmptyWalletDialog', () => {
|
||||
});
|
||||
|
||||
it('should display reset time when provided', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
resetTime="3:45 PM"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('3:45 PM');
|
||||
@@ -106,13 +101,12 @@ describe('EmptyWalletDialog', () => {
|
||||
});
|
||||
|
||||
it('should not display reset time when not provided', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).not.toContain('Access resets at');
|
||||
@@ -120,13 +114,12 @@ describe('EmptyWalletDialog', () => {
|
||||
});
|
||||
|
||||
it('should display slash command hints', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('/stats');
|
||||
@@ -139,14 +132,13 @@ describe('EmptyWalletDialog', () => {
|
||||
describe('onChoice handling', () => {
|
||||
it('should call onGetCredits and onChoice when get_credits is selected', async () => {
|
||||
// get_credits is the first item, so just press Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
onGetCredits={mockOnGetCredits}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
@@ -158,13 +150,12 @@ describe('EmptyWalletDialog', () => {
|
||||
});
|
||||
|
||||
it('should call onChoice without onGetCredits when onGetCredits is not provided', async () => {
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
@@ -177,14 +168,13 @@ describe('EmptyWalletDialog', () => {
|
||||
it('should call onChoice with use_fallback when selected', async () => {
|
||||
// With fallback: items are [get_credits, use_fallback, stop]
|
||||
// use_fallback is the second item: Down + Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-3-flash-preview"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\r');
|
||||
@@ -198,13 +188,12 @@ describe('EmptyWalletDialog', () => {
|
||||
it('should call onChoice with stop when selected', async () => {
|
||||
// Without fallback: items are [get_credits, stop]
|
||||
// stop is the second item: Down + Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<EmptyWalletDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
@@ -440,36 +440,38 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const { stdin, lastFrame } = await renderWithProviders(
|
||||
<BubbleListener>
|
||||
<ExitPlanModeDialog
|
||||
planPath={mockPlanFullPath}
|
||||
onApprove={onApprove}
|
||||
onFeedback={onFeedback}
|
||||
onCancel={onCancel}
|
||||
getPreferredEditor={vi.fn()}
|
||||
width={80}
|
||||
availableHeight={24}
|
||||
/>
|
||||
</BubbleListener>,
|
||||
{
|
||||
config: {
|
||||
getTargetDir: () => mockTargetDir,
|
||||
getIdeMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
storage: {
|
||||
getPlansDir: () => mockPlansDir,
|
||||
},
|
||||
getFileSystemService: (): FileSystemService => ({
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
const { stdin, lastFrame } = await act(async () =>
|
||||
renderWithProviders(
|
||||
<BubbleListener>
|
||||
<ExitPlanModeDialog
|
||||
planPath={mockPlanFullPath}
|
||||
onApprove={onApprove}
|
||||
onFeedback={onFeedback}
|
||||
onCancel={onCancel}
|
||||
getPreferredEditor={vi.fn()}
|
||||
width={80}
|
||||
availableHeight={24}
|
||||
/>
|
||||
</BubbleListener>,
|
||||
{
|
||||
config: {
|
||||
getTargetDir: () => mockTargetDir,
|
||||
getIdeMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
storage: {
|
||||
getPlansDir: () => mockPlansDir,
|
||||
},
|
||||
getFileSystemService: (): FileSystemService => ({
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
}),
|
||||
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
|
||||
} as unknown as import('@google/gemini-cli-core').Config,
|
||||
settings: createMockSettings({
|
||||
ui: { useAlternateBuffer: useAlternateBuffer ?? true },
|
||||
}),
|
||||
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
|
||||
} as unknown as import('@google/gemini-cli-core').Config,
|
||||
settings: createMockSettings({
|
||||
ui: { useAlternateBuffer: useAlternateBuffer ?? true },
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
||||
@@ -24,8 +24,7 @@ describe('ExitWarning', () => {
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<ExitWarning />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<ExitWarning />);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -36,8 +35,7 @@ describe('ExitWarning', () => {
|
||||
ctrlCPressedOnce: true,
|
||||
ctrlDPressedOnce: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<ExitWarning />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<ExitWarning />);
|
||||
expect(lastFrame()).toContain('Press Ctrl+C again to exit');
|
||||
unmount();
|
||||
});
|
||||
@@ -48,8 +46,7 @@ describe('ExitWarning', () => {
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<ExitWarning />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<ExitWarning />);
|
||||
expect(lastFrame()).toContain('Press Ctrl+D again to exit');
|
||||
unmount();
|
||||
});
|
||||
@@ -60,8 +57,7 @@ describe('ExitWarning', () => {
|
||||
ctrlCPressedOnce: true,
|
||||
ctrlDPressedOnce: true,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<ExitWarning />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<ExitWarning />);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -48,10 +48,9 @@ describe('FolderTrustDialog', () => {
|
||||
});
|
||||
|
||||
it('should render the dialog with title and description', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Do you trust the files in this folder?');
|
||||
expect(lastFrame()).toContain(
|
||||
@@ -72,7 +71,7 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: [],
|
||||
securityWarnings: [],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
@@ -85,7 +84,6 @@ describe('FolderTrustDialog', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('This folder contains:');
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
unmount();
|
||||
@@ -103,7 +101,7 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: [],
|
||||
securityWarnings: [],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
@@ -116,7 +114,6 @@ describe('FolderTrustDialog', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
// With maxHeight=4, the intro text (4 lines) will take most of the space.
|
||||
// The discovery results will likely be hidden.
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
@@ -135,7 +132,7 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: [],
|
||||
securityWarnings: [],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
@@ -148,7 +145,6 @@ describe('FolderTrustDialog', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
unmount();
|
||||
});
|
||||
@@ -182,9 +178,7 @@ describe('FolderTrustDialog', () => {
|
||||
// Initial state: truncated
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Do you trust the files in this folder?');
|
||||
// In standard terminal mode, the expansion hint is handled globally by ToastDisplay
|
||||
// via AppContainer, so it should not be present in the dialog's local frame.
|
||||
expect(lastFrame()).not.toContain('Press Ctrl+O');
|
||||
expect(lastFrame()).toContain('Press Ctrl+O');
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
});
|
||||
|
||||
@@ -221,7 +215,6 @@ describe('FolderTrustDialog', () => {
|
||||
await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={onSelect} isRestarting={false} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001b[27u'); // Press kitty escape key
|
||||
@@ -246,10 +239,9 @@ describe('FolderTrustDialog', () => {
|
||||
});
|
||||
|
||||
it('should display restart message when isRestarting is true', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Gemini CLI is restarting');
|
||||
unmount();
|
||||
@@ -260,10 +252,9 @@ describe('FolderTrustDialog', () => {
|
||||
const relaunchApp = vi
|
||||
.spyOn(processUtils, 'relaunchApp')
|
||||
.mockResolvedValue(undefined);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(relaunchApp).toHaveBeenCalled();
|
||||
unmount();
|
||||
@@ -275,10 +266,9 @@ describe('FolderTrustDialog', () => {
|
||||
const relaunchApp = vi
|
||||
.spyOn(processUtils, 'relaunchApp')
|
||||
.mockResolvedValue(undefined);
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} isRestarting={true} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Unmount immediately (before 250ms)
|
||||
unmount();
|
||||
@@ -292,7 +282,6 @@ describe('FolderTrustDialog', () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} isRestarting={false} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('r');
|
||||
@@ -308,30 +297,27 @@ describe('FolderTrustDialog', () => {
|
||||
describe('directory display', () => {
|
||||
it('should correctly display the folder name for a nested directory', async () => {
|
||||
mockedCwd.mockReturnValue('/home/user/project');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Trust folder (project)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should correctly display the parent folder name for a nested directory', async () => {
|
||||
mockedCwd.mockReturnValue('/home/user/project');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Trust parent folder (user)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should correctly display an empty parent folder name for a directory directly under root', async () => {
|
||||
mockedCwd.mockReturnValue('/project');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Trust parent folder ()');
|
||||
unmount();
|
||||
});
|
||||
@@ -348,7 +334,7 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: [],
|
||||
securityWarnings: [],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
@@ -356,7 +342,6 @@ describe('FolderTrustDialog', () => {
|
||||
{ width: 80 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('This folder contains:');
|
||||
expect(lastFrame()).toContain('• Commands (2):');
|
||||
expect(lastFrame()).toContain('- cmd1');
|
||||
@@ -386,14 +371,13 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: [],
|
||||
securityWarnings: ['Dangerous setting detected!'],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Security Warnings:');
|
||||
expect(lastFrame()).toContain('Dangerous setting detected!');
|
||||
unmount();
|
||||
@@ -410,14 +394,13 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: ['Failed to load custom commands'],
|
||||
securityWarnings: [],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Discovery Errors:');
|
||||
expect(lastFrame()).toContain('Failed to load custom commands');
|
||||
unmount();
|
||||
@@ -434,7 +417,7 @@ describe('FolderTrustDialog', () => {
|
||||
discoveryErrors: [],
|
||||
securityWarnings: [],
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
@@ -447,7 +430,6 @@ describe('FolderTrustDialog', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
// In alternate buffer + expanded, the title should be visible (StickyHeader)
|
||||
expect(lastFrame()).toContain('Do you trust the files in this folder?');
|
||||
// And it should NOT use MaxSizedBox truncation
|
||||
@@ -470,7 +452,7 @@ describe('FolderTrustDialog', () => {
|
||||
securityWarnings: [`${ansiRed}warning-with-ansi${ansiReset}`],
|
||||
};
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<FolderTrustDialog
|
||||
onSelect={vi.fn()}
|
||||
discoveryResults={discoveryResults}
|
||||
@@ -478,7 +460,6 @@ describe('FolderTrustDialog', () => {
|
||||
{ width: 100, uiState: { terminalHeight: 40 } },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('cmd-with-ansi');
|
||||
|
||||
@@ -138,33 +138,25 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('renders the component', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
branchName: defaultProps.branchName,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
branchName: defaultProps.branchName,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('path display', () => {
|
||||
it('should display a shortened path on a narrow terminal', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 79,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 79,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
// Should contain some part of the path, likely shortened
|
||||
@@ -173,15 +165,11 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('should use wide layout at 80 columns', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 80,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 80,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
expect(output).toContain(path.join('make', 'it'));
|
||||
@@ -189,28 +177,24 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('should not truncate high-priority items on narrow terminals (regression)', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 60,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
settings: createMockSettings({
|
||||
general: {
|
||||
vimMode: true,
|
||||
},
|
||||
ui: {
|
||||
footer: {
|
||||
showLabels: true,
|
||||
items: ['workspace', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 60,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({
|
||||
general: {
|
||||
vimMode: true,
|
||||
},
|
||||
ui: {
|
||||
footer: {
|
||||
showLabels: true,
|
||||
items: ['workspace', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
const output = lastFrame();
|
||||
// [INSERT] is high priority and should be fully visible
|
||||
// (Note: VimModeProvider defaults to 'INSERT' mode when enabled)
|
||||
@@ -222,168 +206,140 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('displays the branch name when provided', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
branchName: defaultProps.branchName,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
branchName: defaultProps.branchName,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.branchName);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not display the branch name when not provided', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { branchName: undefined, sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { branchName: undefined, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).not.toContain('Branch');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays the model name and context percentage', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: defaultProps.model,
|
||||
sessionStats: {
|
||||
...mockSessionStats,
|
||||
lastPromptTokenCount: 1000,
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: defaultProps.model,
|
||||
sessionStats: {
|
||||
...mockSessionStats,
|
||||
lastPromptTokenCount: 1000,
|
||||
},
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
}),
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\d+% used/);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays the usage indicator when usage is low', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 15,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 15,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame()).toContain('85%');
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides the usage indicator when usage is not near limit', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 85,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 85,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(normalizeFrame(lastFrame())).not.toContain('used');
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays "Limit reached" message when remaining is 0', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame()?.toLowerCase()).toContain('limit reached');
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays the model name and abbreviated context used label on narrow terminals', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 99,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 99,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\d+%/);
|
||||
expect(lastFrame()).not.toContain('context used');
|
||||
@@ -392,33 +348,25 @@ describe('<Footer />', () => {
|
||||
|
||||
describe('sandbox and trust info', () => {
|
||||
it('should display untrusted when isTrustedFolder is false', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('untrusted');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display custom sandbox info when SANDBOX env is set', async () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
isTrustedFolder: undefined,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
isTrustedFolder: undefined,
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame()).toContain('test');
|
||||
vi.unstubAllEnvs();
|
||||
unmount();
|
||||
@@ -427,15 +375,11 @@ describe('<Footer />', () => {
|
||||
it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', async () => {
|
||||
vi.stubEnv('SANDBOX', 'sandbox-exec');
|
||||
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
|
||||
vi.unstubAllEnvs();
|
||||
unmount();
|
||||
@@ -444,15 +388,11 @@ describe('<Footer />', () => {
|
||||
it('should display "no sandbox" when SANDBOX is not set and folder is trusted', async () => {
|
||||
// Clear any SANDBOX env var that might be set.
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('no sandbox');
|
||||
vi.unstubAllEnvs();
|
||||
unmount();
|
||||
@@ -460,15 +400,11 @@ describe('<Footer />', () => {
|
||||
|
||||
it('should prioritize untrusted message over sandbox info', async () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('untrusted');
|
||||
expect(lastFrame()).not.toMatch(/test-sandbox/s);
|
||||
vi.unstubAllEnvs();
|
||||
@@ -478,22 +414,18 @@ describe('<Footer />', () => {
|
||||
|
||||
describe('footer configuration filtering (golden snapshots)', () => {
|
||||
it('renders complete footer with all sections visible (baseline)', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot(
|
||||
'complete-footer-wide',
|
||||
);
|
||||
@@ -523,47 +455,39 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('renders footer with only model info hidden (partial filtering)', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideCWD: false,
|
||||
hideSandboxStatus: false,
|
||||
hideModelInfo: true,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideCWD: false,
|
||||
hideSandboxStatus: false,
|
||||
hideModelInfo: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot('footer-no-model');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders footer with CWD and model info hidden to test alignment (only sandbox visible)', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideCWD: true,
|
||||
hideSandboxStatus: false,
|
||||
hideModelInfo: true,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideCWD: true,
|
||||
hideSandboxStatus: false,
|
||||
hideModelInfo: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot(
|
||||
'footer-only-sandbox',
|
||||
);
|
||||
@@ -571,64 +495,52 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('hides the context percentage when hideContextPercentage is true', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: true,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).not.toMatch(/\d+% used/);
|
||||
unmount();
|
||||
});
|
||||
it('shows the context percentage when hideContextPercentage is false', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\d+% used/);
|
||||
unmount();
|
||||
});
|
||||
it('renders complete footer in narrow terminal (baseline narrow)', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 79,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 79,
|
||||
uiState: { sessionStats: mockSessionStats },
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
},
|
||||
}),
|
||||
});
|
||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot(
|
||||
'complete-footer-narrow',
|
||||
);
|
||||
@@ -714,60 +626,48 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('hides error summary in low verbosity mode out of dev mode', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
errorCount: 2,
|
||||
showErrorDetails: false,
|
||||
},
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
errorCount: 2,
|
||||
showErrorDetails: false,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
|
||||
});
|
||||
expect(lastFrame()).not.toContain('F12 for details');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows error summary in low verbosity mode in dev mode', async () => {
|
||||
mocks.isDevelopment = true;
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
errorCount: 2,
|
||||
showErrorDetails: false,
|
||||
},
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
errorCount: 2,
|
||||
showErrorDetails: false,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'low' } }),
|
||||
});
|
||||
expect(lastFrame()).toContain('F12 for details');
|
||||
expect(lastFrame()).toContain('2 errors');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows error summary in full verbosity mode', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
errorCount: 2,
|
||||
showErrorDetails: false,
|
||||
},
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
errorCount: 2,
|
||||
showErrorDetails: false,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({ ui: { errorVerbosity: 'full' } }),
|
||||
});
|
||||
expect(lastFrame()).toContain('F12 for details');
|
||||
expect(lastFrame()).toContain('2 errors');
|
||||
unmount();
|
||||
@@ -776,25 +676,21 @@ describe('<Footer />', () => {
|
||||
|
||||
describe('Footer Custom Items', () => {
|
||||
it('renders items in the specified order', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['model-name', 'workspace'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
currentModel: 'gemini-pro',
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['model-name', 'workspace'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
const modelIdx = output.indexOf('/model');
|
||||
@@ -804,28 +700,24 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('renders multiple items with proper alignment', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
branchName: 'main',
|
||||
},
|
||||
settings: createMockSettings({
|
||||
vimMode: {
|
||||
vimMode: true,
|
||||
},
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['workspace', 'git-branch', 'sandbox', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
branchName: 'main',
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({
|
||||
vimMode: {
|
||||
vimMode: true,
|
||||
},
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['workspace', 'git-branch', 'sandbox', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
@@ -862,25 +754,21 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('does not render items that are conditionally hidden', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
branchName: undefined, // No branch
|
||||
},
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['workspace', 'git-branch', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
branchName: undefined, // No branch
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
settings: createMockSettings({
|
||||
ui: {
|
||||
footer: {
|
||||
items: ['workspace', 'git-branch', 'model-name'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
@@ -893,18 +781,14 @@ describe('<Footer />', () => {
|
||||
|
||||
describe('fallback mode display', () => {
|
||||
it('should display Flash model when in fallback mode, not the configured Pro model', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
currentModel: 'gemini-2.5-flash', // Fallback active, showing Flash
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
currentModel: 'gemini-2.5-flash', // Fallback active, showing Flash
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
// Footer should show the effective model (Flash), not the config model (Pro)
|
||||
expect(lastFrame()).toContain('gemini-2.5-flash');
|
||||
@@ -913,18 +797,14 @@ describe('<Footer />', () => {
|
||||
});
|
||||
|
||||
it('should display Pro model when NOT in fallback mode', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
currentModel: 'gemini-2.5-pro', // Normal mode, showing Pro
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
currentModel: 'gemini-2.5-pro', // Normal mode, showing Pro
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('gemini-2.5-pro');
|
||||
unmount();
|
||||
|
||||
@@ -30,19 +30,17 @@ describe('<FooterConfigDialog />', () => {
|
||||
{ settings },
|
||||
);
|
||||
|
||||
await renderResult.waitUntilReady();
|
||||
expect(renderResult.lastFrame()).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
});
|
||||
|
||||
it('toggles an item when enter is pressed', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, stdin } = await renderWithProviders(
|
||||
<FooterConfigDialog onClose={mockOnClose} />,
|
||||
{ settings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
act(() => {
|
||||
stdin.write('\r'); // Enter to toggle
|
||||
});
|
||||
@@ -62,12 +60,11 @@ describe('<FooterConfigDialog />', () => {
|
||||
|
||||
it('reorders items with arrow keys', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, stdin } = await renderWithProviders(
|
||||
<FooterConfigDialog onClose={mockOnClose} />,
|
||||
{ settings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
// Initial order: workspace, git-branch, ...
|
||||
const output = lastFrame();
|
||||
const cwdIdx = output.indexOf('] workspace');
|
||||
@@ -93,12 +90,11 @@ describe('<FooterConfigDialog />', () => {
|
||||
|
||||
it('closes on Esc', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { stdin } = await renderWithProviders(
|
||||
<FooterConfigDialog onClose={mockOnClose} />,
|
||||
{ settings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
act(() => {
|
||||
stdin.write('\x1b'); // Esc
|
||||
});
|
||||
@@ -115,9 +111,8 @@ describe('<FooterConfigDialog />', () => {
|
||||
{ settings },
|
||||
);
|
||||
|
||||
const { lastFrame, stdin, waitUntilReady } = renderResult;
|
||||
const { lastFrame, stdin } = renderResult;
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('~/project/path');
|
||||
|
||||
// Move focus down to 'code-changes' (which has colored elements)
|
||||
@@ -148,13 +143,11 @@ describe('<FooterConfigDialog />', () => {
|
||||
|
||||
it('shows an empty preview when all items are deselected', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, stdin } = await renderWithProviders(
|
||||
<FooterConfigDialog onClose={mockOnClose} />,
|
||||
{ settings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// Default items are the first 5. We toggle them off.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
act(() => {
|
||||
@@ -178,11 +171,10 @@ describe('<FooterConfigDialog />', () => {
|
||||
|
||||
it('moves item correctly after trying to move up at the top', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, stdin } = await renderWithProviders(
|
||||
<FooterConfigDialog onClose={mockOnClose} />,
|
||||
{ settings },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Default initial items in mock settings are 'git-branch', 'workspace', ...
|
||||
await waitFor(() => {
|
||||
@@ -222,8 +214,7 @@ describe('<FooterConfigDialog />', () => {
|
||||
{ settings },
|
||||
);
|
||||
|
||||
const { lastFrame, stdin, waitUntilReady } = renderResult;
|
||||
await waitUntilReady();
|
||||
const { lastFrame, stdin } = renderResult;
|
||||
|
||||
// By default labels are on
|
||||
expect(lastFrame()).toContain('workspace (/directory)');
|
||||
|
||||
@@ -41,10 +41,7 @@ describe('GeminiRespondingSpinner', () => {
|
||||
|
||||
it('renders spinner when responding', async () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<GeminiRespondingSpinner />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<GeminiRespondingSpinner />);
|
||||
expect(lastFrame()).toContain('GeminiSpinner');
|
||||
unmount();
|
||||
});
|
||||
@@ -52,30 +49,23 @@ describe('GeminiRespondingSpinner', () => {
|
||||
it('renders screen reader text when responding and screen reader enabled', async () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<GeminiRespondingSpinner />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<GeminiRespondingSpinner />);
|
||||
expect(lastFrame()).toContain(SCREEN_READER_RESPONDING);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders nothing when not responding and no non-responding display', async () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<GeminiRespondingSpinner />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<GeminiRespondingSpinner />);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders non-responding display when provided', async () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<GeminiRespondingSpinner nonRespondingDisplay="Waiting..." />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Waiting...');
|
||||
unmount();
|
||||
});
|
||||
@@ -83,10 +73,9 @@ describe('GeminiRespondingSpinner', () => {
|
||||
it('renders screen reader loading text when non-responding display provided and screen reader enabled', async () => {
|
||||
mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<GeminiRespondingSpinner nonRespondingDisplay="Waiting..." />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(SCREEN_READER_LOADING);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -72,53 +72,46 @@ useSessionStatsMock.mockReturnValue({
|
||||
|
||||
describe('Gradient Crash Regression Tests', () => {
|
||||
it('<Header /> should not crash when theme.ui.gradient is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Header version="1.0.0" nightly={false} />,
|
||||
{
|
||||
width: 120,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('<ModelDialog /> should not crash when theme.ui.gradient is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<ModelDialog onClose={async () => {}} />,
|
||||
{
|
||||
width: 120,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('<Banner /> should not crash when theme.ui.gradient is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Banner bannerText="Test Banner" isWarning={false} width={80} />,
|
||||
{
|
||||
width: 120,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('<Footer /> should not crash when theme.ui.gradient has only one color (or empty) and nightly is true', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Footer />,
|
||||
{
|
||||
width: 120,
|
||||
uiState: {
|
||||
nightly: true, // Enable nightly to trigger Gradient usage logic
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
width: 120,
|
||||
uiState: {
|
||||
nightly: true, // Enable nightly to trigger Gradient usage logic
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
});
|
||||
// If it crashes, this line won't be reached or lastFrame() will throw
|
||||
expect(lastFrame()).toBeDefined();
|
||||
// It should fall back to rendering text without gradient
|
||||
@@ -127,7 +120,7 @@ describe('Gradient Crash Regression Tests', () => {
|
||||
});
|
||||
|
||||
it('<StatsDisplay /> should not crash when theme.ui.gradient is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<StatsDisplay duration="1s" title="My Stats" />,
|
||||
{
|
||||
width: 120,
|
||||
@@ -136,7 +129,6 @@ describe('Gradient Crash Regression Tests', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBeDefined();
|
||||
// Ensure title is rendered
|
||||
expect(lastFrame()).toContain('My Stats');
|
||||
|
||||
@@ -39,12 +39,12 @@ describe('<Header />', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the long logo on a wide terminal', () => {
|
||||
it('renders the long logo on a wide terminal', async () => {
|
||||
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
await render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: longAsciiLogo,
|
||||
@@ -53,9 +53,9 @@ describe('<Header />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders custom ASCII art when provided', () => {
|
||||
it('renders custom ASCII art when provided', async () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
render(
|
||||
await render(
|
||||
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(Text).toHaveBeenCalledWith(
|
||||
@@ -66,8 +66,8 @@ describe('<Header />', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('displays the version number when nightly is true', () => {
|
||||
render(<Header version="1.0.0" nightly={true} />);
|
||||
it('displays the version number when nightly is true', async () => {
|
||||
await render(<Header version="1.0.0" nightly={true} />);
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
const versionText = Array.isArray(textCalls[1][0].children)
|
||||
? textCalls[1][0].children.join('')
|
||||
@@ -75,8 +75,8 @@ describe('<Header />', () => {
|
||||
expect(versionText).toBe('v1.0.0');
|
||||
});
|
||||
|
||||
it('does not display the version number when nightly is false', () => {
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
it('does not display the version number when nightly is false', async () => {
|
||||
await render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Text).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
children: 'v1.0.0',
|
||||
@@ -119,7 +119,7 @@ describe('<Header />', () => {
|
||||
},
|
||||
});
|
||||
const Gradient = await import('ink-gradient');
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
await render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).not.toHaveBeenCalled();
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
expect(textCalls[0][0]).toHaveProperty('color', '#123456');
|
||||
@@ -131,7 +131,7 @@ describe('<Header />', () => {
|
||||
ui: { gradient: [singleColor] },
|
||||
} as typeof semanticColors.theme);
|
||||
const Gradient = await import('ink-gradient');
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
await render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).not.toHaveBeenCalled();
|
||||
const textCalls = (Text as Mock).mock.calls;
|
||||
expect(textCalls.length).toBe(1);
|
||||
@@ -144,7 +144,7 @@ describe('<Header />', () => {
|
||||
ui: { gradient: gradientColors },
|
||||
} as typeof semanticColors.theme);
|
||||
const Gradient = await import('ink-gradient');
|
||||
render(<Header version="1.0.0" nightly={false} />);
|
||||
await render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(Gradient.default).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
colors: gradientColors,
|
||||
|
||||
@@ -43,10 +43,9 @@ const mockCommands: readonly SlashCommand[] = [
|
||||
|
||||
describe('Help Component', () => {
|
||||
it('should not render hidden commands', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<Help commands={mockCommands} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('/test');
|
||||
@@ -55,10 +54,9 @@ describe('Help Component', () => {
|
||||
});
|
||||
|
||||
it('should not render hidden subcommands', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<Help commands={mockCommands} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('visible-child');
|
||||
@@ -67,10 +65,9 @@ describe('Help Component', () => {
|
||||
});
|
||||
|
||||
it('should render keyboard shortcuts', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<Help commands={mockCommands} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Keyboard Shortcuts:');
|
||||
|
||||
@@ -39,10 +39,9 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: MessageType.USER,
|
||||
text: 'Hello',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Hello');
|
||||
unmount();
|
||||
});
|
||||
@@ -53,10 +52,9 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'hint',
|
||||
text: 'Try using ripgrep first',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Try using ripgrep first');
|
||||
unmount();
|
||||
});
|
||||
@@ -67,10 +65,9 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: MessageType.USER,
|
||||
text: '/theme',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('/theme');
|
||||
unmount();
|
||||
});
|
||||
@@ -83,14 +80,13 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: MessageType.INFO,
|
||||
text: '⚡ Line 1\n⚡ Line 2\n⚡ Line 3',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
},
|
||||
@@ -114,10 +110,9 @@ describe('<HistoryItemDisplay />', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -128,12 +123,11 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: MessageType.STATS,
|
||||
duration: '1s',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<SessionStatsProvider>
|
||||
<HistoryItemDisplay {...baseItem} item={item} />
|
||||
</SessionStatsProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Stats');
|
||||
unmount();
|
||||
});
|
||||
@@ -150,10 +144,9 @@ describe('<HistoryItemDisplay />', () => {
|
||||
gcpProject: 'test-project',
|
||||
ideClient: 'test-ide',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('About Gemini CLI');
|
||||
unmount();
|
||||
});
|
||||
@@ -163,12 +156,11 @@ describe('<HistoryItemDisplay />', () => {
|
||||
...baseItem,
|
||||
type: 'model_stats',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<SessionStatsProvider>
|
||||
<HistoryItemDisplay {...baseItem} item={item} />
|
||||
</SessionStatsProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'No API calls have been made in this session.',
|
||||
);
|
||||
@@ -180,12 +172,11 @@ describe('<HistoryItemDisplay />', () => {
|
||||
...baseItem,
|
||||
type: 'tool_stats',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<SessionStatsProvider>
|
||||
<HistoryItemDisplay {...baseItem} item={item} />
|
||||
</SessionStatsProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain(
|
||||
'No tool calls have been made in this session.',
|
||||
);
|
||||
@@ -198,12 +189,11 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'quit',
|
||||
duration: '1s',
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<SessionStatsProvider>
|
||||
<HistoryItemDisplay {...baseItem} item={item} />
|
||||
</SessionStatsProvider>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Agent powering down. Goodbye!');
|
||||
unmount();
|
||||
});
|
||||
@@ -215,14 +205,13 @@ describe('<HistoryItemDisplay />', () => {
|
||||
text: 'Hello, \u001b[31mred\u001b[0m world!',
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={historyItem}
|
||||
terminalWidth={80}
|
||||
isPending={false}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// The ANSI codes should be escaped for display.
|
||||
expect(lastFrame()).toContain('Hello, \\u001b[31mred\\u001b[0m world!');
|
||||
@@ -253,14 +242,13 @@ describe('<HistoryItemDisplay />', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={historyItem}
|
||||
terminalWidth={80}
|
||||
isPending={false}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const passedProps = vi.mocked(ToolGroupMessage).mock.calls[0][0];
|
||||
const confirmationDetails = passedProps.toolCalls[0]
|
||||
@@ -279,13 +267,12 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'thinking',
|
||||
thought: { subject: 'Thinking', description: 'test' },
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { inlineThinkingMode: 'full' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -297,13 +284,12 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'thinking',
|
||||
thought: { subject: 'Thinking', description: 'test' },
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} isFirstThinking={true} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { inlineThinkingMode: 'full' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain(' Thinking...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -315,13 +301,12 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'thinking',
|
||||
thought: { subject: 'Thinking', description: 'test' },
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
{
|
||||
settings: createMockSettings({ ui: { inlineThinkingMode: 'off' } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
@@ -343,21 +328,18 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'gemini',
|
||||
text: longCode,
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } =
|
||||
await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -368,22 +350,19 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'gemini',
|
||||
text: longCode,
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } =
|
||||
await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -394,21 +373,18 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'gemini_content',
|
||||
text: longCode,
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } =
|
||||
await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -419,22 +395,19 @@ describe('<HistoryItemDisplay />', () => {
|
||||
type: 'gemini_content',
|
||||
text: longCode,
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } =
|
||||
await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay
|
||||
item={item}
|
||||
isPending={false}
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={10}
|
||||
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
|
||||
/>,
|
||||
{
|
||||
config: makeFakeConfig({ useAlternateBuffer }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -18,10 +18,9 @@ describe('<HookStatusDisplay />', () => {
|
||||
const props = {
|
||||
activeHooks: [{ name: 'test-hook', eventName: 'BeforeAgent' }],
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<HookStatusDisplay {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -33,10 +32,9 @@ describe('<HookStatusDisplay />', () => {
|
||||
{ name: 'h2', eventName: 'BeforeAgent' },
|
||||
],
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<HookStatusDisplay {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -47,20 +45,18 @@ describe('<HookStatusDisplay />', () => {
|
||||
{ name: 'step', eventName: 'BeforeAgent', index: 1, total: 3 },
|
||||
],
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<HookStatusDisplay {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should return empty string if no active hooks', async () => {
|
||||
const props = { activeHooks: [] };
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<HookStatusDisplay {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -35,20 +35,18 @@ describe('HooksDialog', () => {
|
||||
|
||||
describe('snapshots', () => {
|
||||
it('renders empty hooks dialog', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={[]} onClose={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders single hook with security warning, source, and tips', async () => {
|
||||
const hooks = [createMockHook('test-hook', 'before-tool', true)];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -59,10 +57,9 @@ describe('HooksDialog', () => {
|
||||
createMockHook('hook2', 'before-tool', false),
|
||||
createMockHook('hook3', 'after-agent', true),
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -80,10 +77,9 @@ describe('HooksDialog', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -100,10 +96,9 @@ describe('HooksDialog', () => {
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -112,10 +107,9 @@ describe('HooksDialog', () => {
|
||||
describe('keyboard interaction', () => {
|
||||
it('should call onClose when escape key is pressed', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { waitUntilReady, stdin, unmount } = await renderWithProviders(
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={[]} onClose={onClose} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
stdin.write('\u001b[27u');
|
||||
@@ -137,10 +131,9 @@ describe('HooksDialog', () => {
|
||||
createMockHook('hook1', 'before-tool', true),
|
||||
createMockHook('hook2', 'after-tool', false),
|
||||
];
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={10} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('▲');
|
||||
expect(lastFrame()).not.toContain('▼');
|
||||
@@ -149,10 +142,9 @@ describe('HooksDialog', () => {
|
||||
|
||||
it('should show scroll down indicator when there are more hooks than maxVisibleHooks', async () => {
|
||||
const hooks = createManyHooks(15);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('▼');
|
||||
unmount();
|
||||
@@ -164,7 +156,6 @@ describe('HooksDialog', () => {
|
||||
await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initially should not show up indicator
|
||||
expect(lastFrame()).not.toContain('▲');
|
||||
@@ -185,7 +176,6 @@ describe('HooksDialog', () => {
|
||||
await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Scroll down twice
|
||||
act(() => {
|
||||
@@ -213,7 +203,6 @@ describe('HooksDialog', () => {
|
||||
await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Scroll down many times past the end
|
||||
act(() => {
|
||||
@@ -236,7 +225,6 @@ describe('HooksDialog', () => {
|
||||
await renderWithProviders(
|
||||
<HooksDialog hooks={hooks} onClose={vi.fn()} maxVisibleHooks={5} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Try to scroll up when already at top
|
||||
act(() => {
|
||||
|
||||
@@ -17,10 +17,9 @@ describe('IdeTrustChangeDialog', () => {
|
||||
});
|
||||
|
||||
it('renders the correct message for CONNECTION_CHANGE', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frameText = lastFrame();
|
||||
expect(frameText).toContain(
|
||||
@@ -31,10 +30,9 @@ describe('IdeTrustChangeDialog', () => {
|
||||
});
|
||||
|
||||
it('renders the correct message for TRUST_CHANGE', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="TRUST_CHANGE" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frameText = lastFrame();
|
||||
expect(frameText).toContain(
|
||||
@@ -48,10 +46,9 @@ describe('IdeTrustChangeDialog', () => {
|
||||
const debugLoggerWarnSpy = vi
|
||||
.spyOn(debugLogger, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="NONE" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frameText = lastFrame();
|
||||
expect(frameText).toContain('Workspace trust has changed.');
|
||||
@@ -68,7 +65,6 @@ describe('IdeTrustChangeDialog', () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="NONE" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('r');
|
||||
@@ -86,7 +82,6 @@ describe('IdeTrustChangeDialog', () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('R');
|
||||
@@ -104,7 +99,6 @@ describe('IdeTrustChangeDialog', () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<IdeTrustChangeDialog reason="CONNECTION_CHANGE" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('a');
|
||||
|
||||
@@ -55,20 +55,18 @@ describe('<LoadingIndicator />', () => {
|
||||
};
|
||||
|
||||
it('should render blank when streamingState is Idle and no loading phrase or thought', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame } = await renderWithContext(
|
||||
<LoadingIndicator elapsedTime={5} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })?.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should render spinner, phrase, and time when streamingState is Responding', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame } = await renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('MockRespondingSpinner');
|
||||
expect(output).toContain('Loading...');
|
||||
@@ -80,11 +78,10 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Confirm action',
|
||||
elapsedTime: 10,
|
||||
};
|
||||
const { lastFrame, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.WaitingForConfirmation,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('⠏'); // Static char for WaitingForConfirmation
|
||||
expect(output).toContain('Confirm action');
|
||||
@@ -97,11 +94,10 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Processing data...',
|
||||
elapsedTime: 3,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Processing data...');
|
||||
unmount();
|
||||
});
|
||||
@@ -111,11 +107,10 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Working...',
|
||||
elapsedTime: 60,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('(esc to cancel, 1m)');
|
||||
unmount();
|
||||
});
|
||||
@@ -125,22 +120,20 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Working...',
|
||||
elapsedTime: 125,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('(esc to cancel, 2m 5s)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render rightContent when provided', async () => {
|
||||
const rightContent = <Text>Extra Info</Text>;
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} rightContent={rightContent} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Extra Info');
|
||||
unmount();
|
||||
});
|
||||
@@ -181,7 +174,6 @@ describe('<LoadingIndicator />', () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
<TestWrapper />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })?.trim()).toBe(''); // Initial: Idle (no loading phrase)
|
||||
|
||||
// Transition to Responding
|
||||
@@ -232,11 +224,10 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'Loading...',
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Loading...');
|
||||
unmount();
|
||||
@@ -250,11 +241,10 @@ describe('<LoadingIndicator />', () => {
|
||||
},
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
if (output) {
|
||||
@@ -274,11 +264,10 @@ describe('<LoadingIndicator />', () => {
|
||||
},
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Thinking... Planning the response...');
|
||||
unmount();
|
||||
@@ -293,11 +282,10 @@ describe('<LoadingIndicator />', () => {
|
||||
currentLoadingPhrase: 'This should not be displayed',
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Thinking... ');
|
||||
expect(output).toContain('This should be displayed');
|
||||
@@ -306,20 +294,19 @@ describe('<LoadingIndicator />', () => {
|
||||
});
|
||||
|
||||
it('should not display thought indicator for non-thought loading phrases', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator
|
||||
currentLoadingPhrase="some random tip..."
|
||||
elapsedTime={3}
|
||||
/>,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('Thinking... ');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should truncate long primary text instead of wrapping', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator
|
||||
{...defaultProps}
|
||||
currentLoadingPhrase={
|
||||
@@ -329,7 +316,6 @@ describe('<LoadingIndicator />', () => {
|
||||
StreamingState.Responding,
|
||||
80,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
@@ -337,7 +323,7 @@ describe('<LoadingIndicator />', () => {
|
||||
|
||||
describe('responsive layout', () => {
|
||||
it('should render on a single line on a wide terminal', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator
|
||||
{...defaultProps}
|
||||
rightContent={<Text>Right</Text>}
|
||||
@@ -345,7 +331,6 @@ describe('<LoadingIndicator />', () => {
|
||||
StreamingState.Responding,
|
||||
120,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
// Check for single line output
|
||||
expect(output?.trim().includes('\n')).toBe(false);
|
||||
@@ -356,7 +341,7 @@ describe('<LoadingIndicator />', () => {
|
||||
});
|
||||
|
||||
it('should render on multiple lines on a narrow terminal', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator
|
||||
{...defaultProps}
|
||||
rightContent={<Text>Right</Text>}
|
||||
@@ -364,7 +349,6 @@ describe('<LoadingIndicator />', () => {
|
||||
StreamingState.Responding,
|
||||
79,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
const lines = output?.trim().split('\n');
|
||||
// Expecting 3 lines:
|
||||
@@ -382,23 +366,21 @@ describe('<LoadingIndicator />', () => {
|
||||
});
|
||||
|
||||
it('should use wide layout at 80 columns', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
StreamingState.Responding,
|
||||
80,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()?.trim().includes('\n')).toBe(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should use narrow layout at 79 columns', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithContext(
|
||||
const { lastFrame, unmount } = await renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
StreamingState.Responding,
|
||||
79,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()?.includes('\n')).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -23,10 +23,9 @@ describe('LogoutConfirmationDialog', () => {
|
||||
});
|
||||
|
||||
it('should render the dialog with title, description, and hint', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('You are now signed out');
|
||||
expect(lastFrame()).toContain(
|
||||
@@ -37,10 +36,9 @@ describe('LogoutConfirmationDialog', () => {
|
||||
});
|
||||
|
||||
it('should render RadioButtonSelect with Login and Exit options', async () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { unmount } = await renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalled();
|
||||
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
|
||||
@@ -57,7 +55,6 @@ describe('LogoutConfirmationDialog', () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={onSelect} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -74,7 +71,6 @@ describe('LogoutConfirmationDialog', () => {
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={onSelect} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -91,7 +87,6 @@ describe('LogoutConfirmationDialog', () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<LogoutConfirmationDialog onSelect={onSelect} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
// Send kitty escape key sequence
|
||||
|
||||
@@ -12,21 +12,19 @@ describe('LoopDetectionConfirmation', () => {
|
||||
const onComplete = vi.fn();
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<LoopDetectionConfirmation onComplete={onComplete} />,
|
||||
{ width: 101 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('contains the expected options', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<LoopDetectionConfirmation onComplete={onComplete} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('A potential loop was detected');
|
||||
|
||||
@@ -364,14 +364,9 @@ describe('MainContent', () => {
|
||||
|
||||
it('renders in alternate buffer mode', async () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<MainContent />,
|
||||
{
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(<MainContent />, {
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('AppHeader(full)');
|
||||
expect(output).toContain('Hello');
|
||||
@@ -452,14 +447,9 @@ describe('MainContent', () => {
|
||||
|
||||
it('does not constrain height in alternate buffer mode', async () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<MainContent />,
|
||||
{
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(<MainContent />, {
|
||||
uiState: defaultMockUiState as Partial<UIState>,
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('AppHeader(full)');
|
||||
expect(output).toContain('Hello');
|
||||
@@ -479,16 +469,11 @@ describe('MainContent', () => {
|
||||
staticAreaMaxItemHeight: 5,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<MainContent />,
|
||||
{
|
||||
uiState: uiState as Partial<UIState>,
|
||||
config: makeFakeConfig({ useAlternateBuffer: true }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as Partial<UIState>,
|
||||
config: makeFakeConfig({ useAlternateBuffer: true }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toMatchSnapshot();
|
||||
@@ -507,16 +492,11 @@ describe('MainContent', () => {
|
||||
staticAreaMaxItemHeight: 5,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<MainContent />,
|
||||
{
|
||||
uiState: uiState as unknown as Partial<UIState>,
|
||||
config: makeFakeConfig({ useAlternateBuffer: true }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as unknown as Partial<UIState>,
|
||||
config: makeFakeConfig({ useAlternateBuffer: true }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
|
||||
});
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toMatchSnapshot();
|
||||
@@ -564,14 +544,9 @@ describe('MainContent', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<MainContent />,
|
||||
{
|
||||
uiState: uiState as Partial<UIState>,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { lastFrame, unmount } = await renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as Partial<UIState>,
|
||||
});
|
||||
const output = lastFrame();
|
||||
// Verify Part 1 and Part 2 are rendered.
|
||||
expect(output).toContain('Part 1');
|
||||
@@ -629,7 +604,6 @@ describe('MainContent', () => {
|
||||
const renderResult = await renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as Partial<UIState>,
|
||||
});
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
const output = renderResult.lastFrame();
|
||||
expect(output).toContain('Initial analysis');
|
||||
@@ -732,15 +706,16 @@ describe('MainContent', () => {
|
||||
bannerVisible: false,
|
||||
};
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } =
|
||||
await renderWithProviders(<MainContent />, {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<MainContent />,
|
||||
{
|
||||
uiState: uiState as Partial<UIState>,
|
||||
config: makeFakeConfig({ useAlternateBuffer: isAlternateBuffer }),
|
||||
settings: createMockSettings({
|
||||
ui: { useAlternateBuffer: isAlternateBuffer },
|
||||
}),
|
||||
});
|
||||
await waitUntilReady();
|
||||
},
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
|
||||
|
||||
@@ -30,19 +30,15 @@ describe('MemoryUsageDisplay', () => {
|
||||
});
|
||||
|
||||
it('renders memory usage', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<MemoryUsageDisplay />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<MemoryUsageDisplay />);
|
||||
expect(lastFrame()).toContain('50.0 MB');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('updates memory usage over time', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = await render(
|
||||
<MemoryUsageDisplay />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('50.0 MB');
|
||||
|
||||
vi.mocked(process.memoryUsage).mockReturnValue({
|
||||
|
||||
@@ -115,7 +115,6 @@ describe('<ModelDialog />', () => {
|
||||
settings,
|
||||
},
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -233,7 +233,8 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
});
|
||||
|
||||
// Deduplicate: only show one entry per unique resolved model value.
|
||||
// This is needed because 3 pro and 3.1 pro models can resolve to the same value.
|
||||
// This is needed because 3 pro and 3.1 pro models can resolve to the same
|
||||
// value, depending on the useGemini31 flag.
|
||||
const seen = new Set<string>();
|
||||
return list.filter((option) => {
|
||||
if (seen.has(option.value)) return false;
|
||||
|
||||
@@ -59,11 +59,10 @@ const renderWithMockedStats = async (
|
||||
},
|
||||
} as unknown as LoadedSettings);
|
||||
|
||||
const result = render(
|
||||
const result = await render(
|
||||
<ModelStatsDisplay currentModel={currentModel} />,
|
||||
width,
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -529,14 +528,13 @@ describe('<ModelStatsDisplay />', () => {
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<ModelStatsDisplay
|
||||
selectedAuthType="oauth"
|
||||
userEmail="test@example.com"
|
||||
tier="Pro"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Auth Method:');
|
||||
|
||||
@@ -73,10 +73,9 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('renders the dialog with the list of folders', async () => {
|
||||
const folders = ['/path/to/folder1', '/path/to/folder2'];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<MultiFolderTrustDialog {...defaultProps} folders={folders} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
'Do you trust the following folders being added to this workspace?',
|
||||
@@ -88,10 +87,9 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('calls onComplete and finishAddingDirectories with an error on escape', async () => {
|
||||
const folders = ['/path/to/folder1'];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog {...defaultProps} folders={folders} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const keypressCallback = mockedUseKeypress.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -121,10 +119,9 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('calls finishAddingDirectories with an error and does not add directories when "No" is chosen', async () => {
|
||||
const folders = ['/path/to/folder1'];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog {...defaultProps} folders={folders} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -148,14 +145,13 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('adds directories to workspace context when "Yes" is chosen', async () => {
|
||||
const folders = ['/path/to/folder1', '/path/to/folder2'];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog
|
||||
{...defaultProps}
|
||||
folders={folders}
|
||||
trustedDirs={['/already/trusted']}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -182,10 +178,9 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('adds directories to workspace context and remembers them as trusted when "Yes, and remember" is chosen', async () => {
|
||||
const folders = ['/path/to/folder1'];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog {...defaultProps} folders={folders} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -212,10 +207,9 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('shows submitting message after a choice is made', async () => {
|
||||
const folders = ['/path/to/folder1'];
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog {...defaultProps} folders={folders} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
|
||||
@@ -230,14 +224,13 @@ describe('MultiFolderTrustDialog', () => {
|
||||
|
||||
it('shows an error message and completes when config is missing', async () => {
|
||||
const folders = ['/path/to/folder1'];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog
|
||||
{...defaultProps}
|
||||
folders={folders}
|
||||
config={null as unknown as Config}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -263,14 +256,13 @@ describe('MultiFolderTrustDialog', () => {
|
||||
});
|
||||
|
||||
const folders = ['/path/to/good', '/path/to/error'];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
const { waitUntilReady, unmount } = await render(
|
||||
<MultiFolderTrustDialog
|
||||
{...defaultProps}
|
||||
folders={folders}
|
||||
errors={['initial error']}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const { onSelect } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
|
||||
@@ -49,10 +49,9 @@ describe('NewAgentsNotification', () => {
|
||||
const onSelect = vi.fn();
|
||||
|
||||
it('renders agent list', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<NewAgentsNotification agents={mockAgents} onSelect={onSelect} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toMatchSnapshot();
|
||||
@@ -68,10 +67,9 @@ describe('NewAgentsNotification', () => {
|
||||
inputConfig: { inputSchema: {} },
|
||||
}));
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<NewAgentsNotification agents={manyAgents} onSelect={onSelect} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toMatchSnapshot();
|
||||
|
||||
@@ -111,14 +111,13 @@ describe('Notifications', () => {
|
||||
});
|
||||
|
||||
it('renders nothing when no notifications', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
settings,
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -137,7 +136,7 @@ describe('Notifications', () => {
|
||||
version: '1.0.0',
|
||||
} as AppState;
|
||||
mockUseAppContext.mockReturnValue(appState);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
appState,
|
||||
@@ -145,7 +144,6 @@ describe('Notifications', () => {
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
warnings.forEach((warning) => {
|
||||
expect(output).toContain(warning.message);
|
||||
@@ -163,15 +161,11 @@ describe('Notifications', () => {
|
||||
} as AppState;
|
||||
mockUseAppContext.mockReturnValue(appState);
|
||||
|
||||
const { waitUntilReady, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
appState,
|
||||
settings,
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { unmount } = await renderWithProviders(<Notifications />, {
|
||||
appState,
|
||||
settings,
|
||||
width: 100,
|
||||
});
|
||||
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
'startupWarningCounts',
|
||||
@@ -199,7 +193,7 @@ describe('Notifications', () => {
|
||||
startupWarningCounts: { 'low-1': 3 },
|
||||
});
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
appState,
|
||||
@@ -207,7 +201,6 @@ describe('Notifications', () => {
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Low priority 1');
|
||||
expect(output).toContain('High priority 1');
|
||||
@@ -234,7 +227,6 @@ describe('Notifications', () => {
|
||||
settings,
|
||||
width: 100,
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('High priority 1');
|
||||
|
||||
await act(async () => {
|
||||
@@ -253,7 +245,7 @@ describe('Notifications', () => {
|
||||
updateInfo: null,
|
||||
} as unknown as UIState;
|
||||
mockUseUIState.mockReturnValue(uiState);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
uiState,
|
||||
@@ -261,7 +253,6 @@ describe('Notifications', () => {
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -273,7 +264,7 @@ describe('Notifications', () => {
|
||||
updateInfo: null,
|
||||
} as unknown as UIState;
|
||||
mockUseUIState.mockReturnValue(uiState);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
uiState,
|
||||
@@ -281,7 +272,6 @@ describe('Notifications', () => {
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -293,7 +283,7 @@ describe('Notifications', () => {
|
||||
updateInfo: { message: 'Update available' },
|
||||
} as unknown as UIState;
|
||||
mockUseUIState.mockReturnValue(uiState);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
uiState,
|
||||
@@ -301,7 +291,6 @@ describe('Notifications', () => {
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -311,14 +300,13 @@ describe('Notifications', () => {
|
||||
persistentStateMock.setData({ hasSeenScreenReaderNudge: false });
|
||||
mockFsAccess.mockRejectedValue(new Error('No legacy file'));
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
settings,
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('screen reader-friendly view');
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
@@ -352,14 +340,13 @@ describe('Notifications', () => {
|
||||
mockUseIsScreenReaderEnabled.mockReturnValue(true);
|
||||
persistentStateMock.setData({ hasSeenScreenReaderNudge: true });
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<Notifications />,
|
||||
{
|
||||
settings,
|
||||
width: 100,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
expect(persistentStateMock.set).not.toHaveBeenCalled();
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('OverageMenuDialog', () => {
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should match snapshot with fallback available', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-3-flash-preview"
|
||||
@@ -38,36 +38,30 @@ describe('OverageMenuDialog', () => {
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should match snapshot without fallback', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={500}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display the credit balance', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={200}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('200');
|
||||
expect(output).toContain('AI Credits available');
|
||||
@@ -75,15 +69,13 @@ describe('OverageMenuDialog', () => {
|
||||
});
|
||||
|
||||
it('should display the model name', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={100}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('gemini-2.5-pro');
|
||||
expect(output).toContain('Usage limit reached');
|
||||
@@ -91,7 +83,7 @@ describe('OverageMenuDialog', () => {
|
||||
});
|
||||
|
||||
it('should display reset time when provided', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
resetTime="3:45 PM"
|
||||
@@ -99,8 +91,6 @@ describe('OverageMenuDialog', () => {
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('3:45 PM');
|
||||
expect(output).toContain('Access resets at');
|
||||
@@ -108,30 +98,26 @@ describe('OverageMenuDialog', () => {
|
||||
});
|
||||
|
||||
it('should not display reset time when not provided', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={100}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).not.toContain('Access resets at');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display slash command hints', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={100}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame() ?? '';
|
||||
expect(output).toContain('/stats');
|
||||
expect(output).toContain('/model');
|
||||
@@ -143,15 +129,13 @@ describe('OverageMenuDialog', () => {
|
||||
describe('onChoice handling', () => {
|
||||
it('should call onChoice with use_credits when selected', async () => {
|
||||
// use_credits is the first item, so just press Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={100}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -162,15 +146,13 @@ describe('OverageMenuDialog', () => {
|
||||
|
||||
it('should call onChoice with manage when selected', async () => {
|
||||
// manage is the second item: Down + Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={100}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
@@ -183,7 +165,7 @@ describe('OverageMenuDialog', () => {
|
||||
it('should call onChoice with use_fallback when selected', async () => {
|
||||
// With fallback: items are [use_credits, manage, use_fallback, stop]
|
||||
// use_fallback is the third item: Down x2 + Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-3-flash-preview"
|
||||
@@ -191,8 +173,6 @@ describe('OverageMenuDialog', () => {
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\r');
|
||||
@@ -206,15 +186,13 @@ describe('OverageMenuDialog', () => {
|
||||
it('should call onChoice with stop when selected', async () => {
|
||||
// Without fallback: items are [use_credits, manage, stop]
|
||||
// stop is the third item: Down x2 + Enter
|
||||
const { unmount, stdin, waitUntilReady } = await renderWithProviders(
|
||||
const { unmount, stdin } = await renderWithProviders(
|
||||
<OverageMenuDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
creditBalance={100}
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\x1b[B'); // Down arrow
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
@@ -72,10 +72,9 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
});
|
||||
|
||||
it('should render the main dialog with current trust level', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Modify Trust Level');
|
||||
@@ -96,10 +95,9 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
commitTrustLevelChange: mockCommitTrustLevelChange,
|
||||
isFolderTrustEnabled: true,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(
|
||||
@@ -120,10 +118,9 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
commitTrustLevelChange: mockCommitTrustLevelChange,
|
||||
isFolderTrustEnabled: true,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain(
|
||||
@@ -134,10 +131,9 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
});
|
||||
|
||||
it('should render the labels with folder names', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={vi.fn()} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Trust this folder (dir)');
|
||||
@@ -152,7 +148,6 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
|
||||
@@ -191,7 +186,6 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
|
||||
@@ -226,7 +220,6 @@ describe('PermissionsModifyTrustDialog', () => {
|
||||
await renderWithProviders(
|
||||
<PermissionsModifyTrustDialog onExit={onExit} addItem={vi.fn()} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => expect(lastFrame()).not.toContain('Loading...'));
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('PolicyUpdateDialog', () => {
|
||||
});
|
||||
|
||||
it('renders correctly and matches snapshot', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<PolicyUpdateDialog
|
||||
config={mockConfig}
|
||||
request={mockRequest}
|
||||
@@ -65,7 +65,6 @@ describe('PolicyUpdateDialog', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toMatchSnapshot();
|
||||
expect(output).toContain('New or changed workspace policies detected');
|
||||
|
||||
@@ -29,8 +29,8 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
describe('for flash model failures', () => {
|
||||
it('should render "Keep trying" and "Stop" options', () => {
|
||||
const { unmount } = render(
|
||||
it('should render "Keep trying" and "Stop" options', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel={DEFAULT_GEMINI_FLASH_MODEL}
|
||||
fallbackModel={DEFAULT_GEMINI_FLASH_MODEL}
|
||||
@@ -63,8 +63,8 @@ describe('ProQuotaDialog', () => {
|
||||
|
||||
describe('for non-flash model failures', () => {
|
||||
describe('when it is a terminal quota error', () => {
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE', () => {
|
||||
const { unmount } = render(
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -101,8 +101,8 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render upgrade option for USE_GEMINI', () => {
|
||||
const { unmount } = render(
|
||||
it('should NOT render upgrade option for USE_GEMINI', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -134,8 +134,8 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render "Keep trying" and "Stop" options when failed model and fallback model are the same', () => {
|
||||
const { unmount } = render(
|
||||
it('should render "Keep trying" and "Stop" options when failed model and fallback model are the same', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel={PREVIEW_GEMINI_MODEL}
|
||||
fallbackModel={PREVIEW_GEMINI_MODEL}
|
||||
@@ -165,8 +165,8 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE (free tier)', () => {
|
||||
const { unmount } = render(
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE (free tier)', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -203,8 +203,8 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render upgrade option for LOGIN_WITH_GOOGLE if tier is Ultra', () => {
|
||||
const { unmount } = render(
|
||||
it('should NOT render upgrade option for LOGIN_WITH_GOOGLE if tier is Ultra', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -239,8 +239,8 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
describe('when it is a capacity error', () => {
|
||||
it('should render keep trying, switch, and stop options', () => {
|
||||
const { unmount } = render(
|
||||
it('should render keep trying, switch, and stop options', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
@@ -274,8 +274,8 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
describe('when it is a model not found error', () => {
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE', () => {
|
||||
const { unmount } = render(
|
||||
it('should render switch, upgrade, and stop options for LOGIN_WITH_GOOGLE', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-3-pro-preview"
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
@@ -312,8 +312,8 @@ describe('ProQuotaDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render upgrade option for USE_GEMINI', () => {
|
||||
const { unmount } = render(
|
||||
it('should NOT render upgrade option for USE_GEMINI', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-3-pro-preview"
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
@@ -348,8 +348,8 @@ describe('ProQuotaDialog', () => {
|
||||
});
|
||||
|
||||
describe('onChoice handling', () => {
|
||||
it('should call onChoice with the selected value', () => {
|
||||
const { unmount } = render(
|
||||
it('should call onChoice with the selected value', async () => {
|
||||
const { unmount } = await render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
|
||||
@@ -10,20 +10,18 @@ import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||
|
||||
describe('QueuedMessageDisplay', () => {
|
||||
it('renders nothing when message queue is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QueuedMessageDisplay messageQueue={[]} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays single queued message', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QueuedMessageDisplay messageQueue={['First message']} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Queued (press ↑ to edit):');
|
||||
@@ -38,10 +36,9 @@ describe('QueuedMessageDisplay', () => {
|
||||
'Third queued message',
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QueuedMessageDisplay messageQueue={messageQueue} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Queued (press ↑ to edit):');
|
||||
@@ -60,10 +57,9 @@ describe('QueuedMessageDisplay', () => {
|
||||
'Message 5',
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QueuedMessageDisplay messageQueue={messageQueue} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Queued (press ↑ to edit):');
|
||||
@@ -79,10 +75,9 @@ describe('QueuedMessageDisplay', () => {
|
||||
it('normalizes whitespace in messages', async () => {
|
||||
const messageQueue = ['Message with\tmultiple\n whitespace'];
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QueuedMessageDisplay messageQueue={messageQueue} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Queued (press ↑ to edit):');
|
||||
|
||||
@@ -43,8 +43,7 @@ describe('QuittingDisplay', () => {
|
||||
mockUseUIState.mockReturnValue({
|
||||
quittingMessages: null,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<QuittingDisplay />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<QuittingDisplay />);
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -58,8 +57,7 @@ describe('QuittingDisplay', () => {
|
||||
quittingMessages: mockMessages,
|
||||
constrainHeight: false,
|
||||
} as unknown as UIState);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(<QuittingDisplay />);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<QuittingDisplay />);
|
||||
expect(lastFrame()).toContain('Goodbye');
|
||||
expect(lastFrame()).toContain('See you later');
|
||||
unmount();
|
||||
|
||||
@@ -20,72 +20,65 @@ describe('QuotaDisplay', () => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
it('should not render when remaining is undefined', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={undefined} limit={100} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render when limit is undefined', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={100} limit={undefined} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render when limit is 0', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={100} limit={0} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render when usage < 80%', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={85} limit={100} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render warning when used >= 80%', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={15} limit={100} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render critical when used >= 95%', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={4} limit={100} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render with reset time when provided', async () => {
|
||||
const resetTime = new Date(Date.now() + 3600000).toISOString(); // 1 hour from now
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={15} limit={100} resetTime={resetTime} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render reset time when terse is true', async () => {
|
||||
const resetTime = new Date(Date.now() + 3600000).toISOString();
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay
|
||||
remaining={15}
|
||||
limit={100}
|
||||
@@ -93,16 +86,14 @@ describe('QuotaDisplay', () => {
|
||||
terse={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render terse limit reached message', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
const { lastFrame, unmount } = await render(
|
||||
<QuotaDisplay remaining={0} limit={100} terse={true} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -24,10 +24,7 @@ describe('RawMarkdownIndicator', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<RawMarkdownIndicator />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<RawMarkdownIndicator />);
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
expect(lastFrame()).toContain('Option+M to toggle');
|
||||
unmount();
|
||||
@@ -37,10 +34,7 @@ describe('RawMarkdownIndicator', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<RawMarkdownIndicator />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { lastFrame, unmount } = await render(<RawMarkdownIndicator />);
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
expect(lastFrame()).toContain('Alt+M to toggle');
|
||||
unmount();
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('RewindConfirmation', () => {
|
||||
details: [{ fileName: 'test.ts', diff: '' }],
|
||||
};
|
||||
const onConfirm = vi.fn();
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindConfirmation
|
||||
stats={stats}
|
||||
onConfirm={onConfirm}
|
||||
@@ -31,7 +31,6 @@ describe('RewindConfirmation', () => {
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(lastFrame()).toContain('Revert code changes');
|
||||
@@ -40,7 +39,7 @@ describe('RewindConfirmation', () => {
|
||||
|
||||
it('renders correctly without stats', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindConfirmation
|
||||
stats={null}
|
||||
onConfirm={onConfirm}
|
||||
@@ -48,7 +47,6 @@ describe('RewindConfirmation', () => {
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(lastFrame()).not.toContain('Revert code changes');
|
||||
@@ -58,7 +56,7 @@ describe('RewindConfirmation', () => {
|
||||
|
||||
it('calls onConfirm with Cancel on Escape', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const { stdin, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<RewindConfirmation
|
||||
stats={null}
|
||||
onConfirm={onConfirm}
|
||||
@@ -66,7 +64,6 @@ describe('RewindConfirmation', () => {
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1b');
|
||||
@@ -81,7 +78,7 @@ describe('RewindConfirmation', () => {
|
||||
it('renders timestamp when provided', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const timestamp = new Date().toISOString();
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindConfirmation
|
||||
stats={null}
|
||||
onConfirm={onConfirm}
|
||||
@@ -90,7 +87,6 @@ describe('RewindConfirmation', () => {
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(lastFrame()).not.toContain('Revert code changes');
|
||||
|
||||
@@ -91,14 +91,13 @@ describe('RewindViewer', () => {
|
||||
const conversation = createConversation([
|
||||
{ type: 'user', content: 'Hello', id: '1', timestamp: '1' },
|
||||
]);
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindViewer
|
||||
conversation={conversation}
|
||||
onExit={vi.fn()}
|
||||
onRewind={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Rewind');
|
||||
expect(lastFrame()).toContain('Hello');
|
||||
unmount();
|
||||
@@ -130,14 +129,13 @@ describe('RewindViewer', () => {
|
||||
const conversation = createConversation(messages as MessageRecord[]);
|
||||
const onExit = vi.fn();
|
||||
const onRewind = vi.fn();
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindViewer
|
||||
conversation={conversation}
|
||||
onExit={onExit}
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -162,7 +160,6 @@ describe('RewindViewer', () => {
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initial state
|
||||
expect(lastFrame()).toMatchSnapshot('initial-state');
|
||||
@@ -197,7 +194,6 @@ describe('RewindViewer', () => {
|
||||
onRewind={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
stdin.write(sequence);
|
||||
@@ -230,7 +226,6 @@ describe('RewindViewer', () => {
|
||||
onRewind={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Up from first -> Last
|
||||
act(() => {
|
||||
@@ -308,7 +303,6 @@ describe('RewindViewer', () => {
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Select
|
||||
await act(async () => {
|
||||
@@ -366,7 +360,6 @@ describe('RewindViewer', () => {
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
|
||||
@@ -403,14 +396,13 @@ describe('RewindViewer', () => {
|
||||
const onExit = vi.fn();
|
||||
const onRewind = vi.fn();
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindViewer
|
||||
conversation={conversation}
|
||||
onExit={onExit}
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot('initial');
|
||||
|
||||
@@ -422,18 +414,14 @@ describe('RewindViewer', () => {
|
||||
];
|
||||
conversation = createConversation(newMessages);
|
||||
|
||||
const {
|
||||
lastFrame: lastFrame2,
|
||||
waitUntilReady: waitUntilReady2,
|
||||
unmount: unmount2,
|
||||
} = await renderWithProviders(
|
||||
<RewindViewer
|
||||
conversation={conversation}
|
||||
onExit={onExit}
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady2();
|
||||
const { lastFrame: lastFrame2, unmount: unmount2 } =
|
||||
await renderWithProviders(
|
||||
<RewindViewer
|
||||
conversation={conversation}
|
||||
onExit={onExit}
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame2()).toMatchSnapshot('after-update');
|
||||
unmount2();
|
||||
@@ -451,15 +439,13 @@ it('renders accessible screen reader view when screen reader is enabled', async
|
||||
const onExit = vi.fn();
|
||||
const onRewind = vi.fn();
|
||||
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<RewindViewer
|
||||
conversation={conversation}
|
||||
onExit={onExit}
|
||||
onRewind={onRewind}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame();
|
||||
expect(frame).toContain('Rewind - Select a conversation point:');
|
||||
expect(frame).toContain('Stay at current position');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user