feat(tmux): collapse windows setup steps by default
This commit is contained in:
parent
a9668ff15d
commit
dd7b729520
4 changed files with 175 additions and 3 deletions
|
|
@ -26,6 +26,7 @@ export interface TmuxInstallerBannerViewModel {
|
|||
progressPercent: number | null;
|
||||
logs: string[];
|
||||
manualHints: TmuxInstallHint[];
|
||||
manualHintsCollapsible: boolean;
|
||||
primaryGuideUrl: string | null;
|
||||
installSupported: boolean;
|
||||
installDisabled: boolean;
|
||||
|
|
@ -79,6 +80,8 @@ export class TmuxInstallerBannerAdapter {
|
|||
: null;
|
||||
const versionLabel =
|
||||
status?.effective.version ?? status?.host.version ?? status?.wsl?.tmuxVersion ?? null;
|
||||
const manualHints = status?.autoInstall.manualHints ?? [];
|
||||
const manualHintsCollapsible = status?.platform === 'win32' && manualHints.length > 0;
|
||||
const installLabel =
|
||||
snapshot.phase === 'idle' &&
|
||||
status?.platform === 'win32' &&
|
||||
|
|
@ -104,7 +107,8 @@ export class TmuxInstallerBannerAdapter {
|
|||
phase: snapshot.phase,
|
||||
progressPercent: formatTmuxInstallerProgress(snapshot.phase),
|
||||
logs: snapshot.logs,
|
||||
manualHints: status?.autoInstall.manualHints ?? [],
|
||||
manualHints,
|
||||
manualHintsCollapsible,
|
||||
primaryGuideUrl,
|
||||
installSupported: status?.autoInstall.supported ?? false,
|
||||
installDisabled:
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ describe('TmuxInstallerBannerAdapter', () => {
|
|||
expect(result.primaryGuideUrl).toBeNull();
|
||||
expect(result.progressPercent).toBeNull();
|
||||
expect(result.manualHints).toHaveLength(1);
|
||||
expect(result.manualHintsCollapsible).toBe(false);
|
||||
expect(result.body).toContain('persistent teammate reliability');
|
||||
});
|
||||
|
||||
|
|
@ -143,6 +144,7 @@ describe('TmuxInstallerBannerAdapter', () => {
|
|||
expect(result.platformLabel).toBe('Windows');
|
||||
expect(result.primaryGuideUrl).toBe('https://learn.microsoft.com/en-us/windows/wsl/install');
|
||||
expect(result.progressPercent).toBe(100);
|
||||
expect(result.manualHintsCollapsible).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the banner visible when tmux is installed but runtime is not ready yet', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
import { AlertTriangle, ExternalLink, RefreshCw, Wrench, XCircle } from 'lucide-react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ExternalLink,
|
||||
RefreshCw,
|
||||
Wrench,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner';
|
||||
|
||||
|
|
@ -28,6 +36,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
const { viewModel, install, cancel, submitInput, refresh, toggleDetails, openExternal } =
|
||||
useTmuxInstallerBanner();
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const [manualHintsExpanded, setManualHintsExpanded] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!viewModel.acceptsInput) {
|
||||
|
|
@ -35,10 +44,19 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
}
|
||||
}, [viewModel.acceptsInput]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!viewModel.manualHintsCollapsible) {
|
||||
setManualHintsExpanded(false);
|
||||
}
|
||||
}, [viewModel.manualHintsCollapsible]);
|
||||
|
||||
if (!viewModel.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const manualHintsVisible =
|
||||
viewModel.manualHints.length > 0 && (!viewModel.manualHintsCollapsible || manualHintsExpanded);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mb-6 rounded-lg border-l-4 px-4 py-3"
|
||||
|
|
@ -114,6 +132,23 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
Manual guide
|
||||
</button>
|
||||
)}
|
||||
{viewModel.manualHintsCollapsible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setManualHintsExpanded((current) => !current)}
|
||||
className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors hover:bg-white/5"
|
||||
style={{ borderColor: 'var(--color-border)' }}
|
||||
>
|
||||
{manualHintsExpanded ? (
|
||||
<ChevronUp className="size-4" />
|
||||
) : (
|
||||
<ChevronDown className="size-4" />
|
||||
)}
|
||||
{manualHintsExpanded
|
||||
? 'Hide setup steps'
|
||||
: `Show setup steps (${viewModel.manualHints.length})`}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void refresh()}
|
||||
|
|
@ -193,7 +228,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{viewModel.manualHints.length > 0 && (
|
||||
{manualHintsVisible && (
|
||||
<div className="mt-3 grid gap-2 lg:grid-cols-2">
|
||||
{viewModel.manualHints.map((hint) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { TmuxInstallerBannerView } from '../TmuxInstallerBannerView';
|
||||
|
||||
import type { TmuxInstallerBannerViewModel } from '../../adapters/TmuxInstallerBannerAdapter';
|
||||
|
||||
const { mockUseTmuxInstallerBanner } = vi.hoisted(() => ({
|
||||
mockUseTmuxInstallerBanner: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useTmuxInstallerBanner', () => ({
|
||||
useTmuxInstallerBanner: mockUseTmuxInstallerBanner,
|
||||
}));
|
||||
|
||||
const baseViewModel: TmuxInstallerBannerViewModel = {
|
||||
visible: true,
|
||||
loading: false,
|
||||
title: 'tmux is not installed',
|
||||
body: 'WSL is available, but no Linux distribution is installed yet.',
|
||||
error: null,
|
||||
platformLabel: 'Windows',
|
||||
locationLabel: null,
|
||||
runtimeReadyLabel: null,
|
||||
versionLabel: null,
|
||||
phase: 'idle',
|
||||
progressPercent: null,
|
||||
logs: [],
|
||||
manualHints: [
|
||||
{
|
||||
title: 'Install WSL',
|
||||
description: 'Install Windows Subsystem for Linux.',
|
||||
command: 'wsl --install --no-distribution',
|
||||
},
|
||||
{
|
||||
title: 'Install Ubuntu',
|
||||
description: 'Recommended WSL distro.',
|
||||
command: 'wsl --install -d Ubuntu --no-launch',
|
||||
},
|
||||
],
|
||||
manualHintsCollapsible: true,
|
||||
primaryGuideUrl: 'https://example.com/guide',
|
||||
installSupported: true,
|
||||
installDisabled: false,
|
||||
installLabel: 'Install Ubuntu in WSL',
|
||||
canCancel: false,
|
||||
acceptsInput: false,
|
||||
inputPrompt: null,
|
||||
inputSecret: false,
|
||||
detailsOpen: false,
|
||||
};
|
||||
|
||||
function renderBanner(viewModel: TmuxInstallerBannerViewModel): {
|
||||
host: HTMLDivElement;
|
||||
root: ReturnType<typeof createRoot>;
|
||||
} {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
mockUseTmuxInstallerBanner.mockReturnValue({
|
||||
viewModel,
|
||||
install: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
submitInput: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
toggleDetails: vi.fn(),
|
||||
openExternal: vi.fn(),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
root.render(React.createElement(TmuxInstallerBannerView));
|
||||
});
|
||||
|
||||
return { host, root };
|
||||
}
|
||||
|
||||
describe('TmuxInstallerBannerView', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockUseTmuxInstallerBanner.mockReset();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('keeps Windows setup steps collapsed by default and expands them on demand', async () => {
|
||||
const { host, root } = renderBanner(baseViewModel);
|
||||
|
||||
expect(host.textContent).toContain('Show setup steps (2)');
|
||||
expect(host.textContent).not.toContain('wsl --install --no-distribution');
|
||||
|
||||
const toggleButton = [...host.querySelectorAll('button')].find((button) =>
|
||||
button.textContent?.includes('Show setup steps')
|
||||
);
|
||||
expect(toggleButton).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
toggleButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Hide setup steps');
|
||||
expect(host.textContent).toContain('wsl --install --no-distribution');
|
||||
expect(host.textContent).toContain('wsl --install -d Ubuntu --no-launch');
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows setup hints immediately on non-Windows platforms', () => {
|
||||
const { host, root } = renderBanner({
|
||||
...baseViewModel,
|
||||
platformLabel: 'macOS',
|
||||
manualHintsCollapsible: false,
|
||||
manualHints: [
|
||||
{ title: 'Homebrew', description: 'Recommended', command: 'brew install tmux' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('brew install tmux');
|
||||
expect(host.textContent).not.toContain('Show setup steps');
|
||||
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue