mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(web): improve repository dropdown search (#2381)
* fix(web): make repo dropdown scrollable * fix(web): add repository dropdown search * fix(web): filter repositories by name only * fix(web): key repository rows by path * chore(web): apply prettier formatting * chore(web): apply ci autofix formatting --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
This commit is contained in:
parent
7d36845575
commit
950c0a7b93
4 changed files with 321 additions and 125 deletions
|
|
@ -71,6 +71,7 @@ export const Header = ({
|
|||
setHelpDialogBoxOpen,
|
||||
} = useAppState();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [repoSearchQuery, setRepoSearchQuery] = useState('');
|
||||
const [isRepoDropdownOpen, setIsRepoDropdownOpen] = useState(false);
|
||||
const [showAnalyzer, setShowAnalyzer] = useState(false);
|
||||
const [reanalyzing, setReanalyzing] = useState<string | null>(null); // repo name being re-analyzed
|
||||
|
|
@ -96,6 +97,13 @@ export const Header = ({
|
|||
.slice(0, 10); // Limit to 10 results
|
||||
}, [graph, searchQuery]);
|
||||
|
||||
const filteredRepos = useMemo(() => {
|
||||
const query = repoSearchQuery.trim().toLowerCase();
|
||||
if (!query) return availableRepos;
|
||||
|
||||
return availableRepos.filter((repo) => repo.name.toLowerCase().includes(query));
|
||||
}, [availableRepos, repoSearchQuery]);
|
||||
|
||||
// Handle clicking outside search or repo dropdown to close them
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
|
|
@ -105,6 +113,7 @@ export const Header = ({
|
|||
if (repoDropdownRef.current && !repoDropdownRef.current.contains(e.target as Node)) {
|
||||
setIsRepoDropdownOpen(false);
|
||||
setShowAnalyzer(false);
|
||||
setRepoSearchQuery('');
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
|
@ -179,8 +188,10 @@ export const Header = ({
|
|||
<div className="relative" ref={repoDropdownRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRepoDropdownOpen((prev) => !prev);
|
||||
const nextOpen = !isRepoDropdownOpen;
|
||||
setIsRepoDropdownOpen(nextOpen);
|
||||
setShowAnalyzer(false);
|
||||
if (!nextOpen) setRepoSearchQuery('');
|
||||
}}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-1.5 text-sm transition-all ${
|
||||
isRepoDropdownOpen
|
||||
|
|
@ -196,145 +207,168 @@ export const Header = ({
|
|||
</button>
|
||||
|
||||
{isRepoDropdownOpen && (
|
||||
<div className="absolute top-full left-0 z-50 mt-1.5 w-80 animate-slide-up overflow-hidden rounded-xl border border-border-subtle bg-surface shadow-xl">
|
||||
<div className="absolute top-full left-0 z-50 mt-1.5 flex max-h-[calc(100vh-4.5rem)] w-80 animate-slide-up flex-col overflow-hidden rounded-xl border border-border-subtle bg-surface shadow-xl">
|
||||
{showAnalyzer ? (
|
||||
<div className="p-4">
|
||||
<div className="scrollbar-thin overflow-y-auto p-4">
|
||||
<RepoAnalyzer
|
||||
variant="sheet"
|
||||
onComplete={(repoName) => {
|
||||
setShowAnalyzer(false);
|
||||
setIsRepoDropdownOpen(false);
|
||||
setRepoSearchQuery('');
|
||||
onAnalyzeComplete?.(repoName);
|
||||
}}
|
||||
onCancel={() => setShowAnalyzer(false)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{/* Repo list */}
|
||||
{availableRepos.length > 0 && (
|
||||
<div>
|
||||
<div className="px-3 pt-2.5 pb-1.5 text-[10px] font-medium tracking-wider text-text-muted uppercase">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="shrink-0 px-3 pt-2.5 pb-1.5 text-[10px] font-medium tracking-wider text-text-muted uppercase">
|
||||
{t('header:repositories')}
|
||||
</div>
|
||||
{availableRepos.map((repo) => (
|
||||
<div
|
||||
key={repo.name}
|
||||
className={`group flex items-center gap-2 px-4 py-2 transition-colors ${
|
||||
repo.name === projectName
|
||||
? 'border-l-2 border-accent bg-accent/10'
|
||||
: 'hover:bg-hover'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (repo.name !== projectName) onSwitchRepo?.(repo.name);
|
||||
setIsRepoDropdownOpen(false);
|
||||
}}
|
||||
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 text-left"
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5 shrink-0 text-node-folder" />
|
||||
<span className="flex-1 truncate font-mono text-sm text-text-primary">
|
||||
{repo.name}
|
||||
</span>
|
||||
{repo.name === projectName && (
|
||||
<span className="shrink-0 font-mono text-[10px] text-accent">
|
||||
{t('header:active')}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{/* Re-analyze */}
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (reanalyzing) return; // already running
|
||||
setReanalyzing(repo.name);
|
||||
setReanalyzeProgress({
|
||||
phase: 'queued',
|
||||
percent: 0,
|
||||
message: t('common:progress.starting'),
|
||||
});
|
||||
try {
|
||||
const { jobId } = await startAnalyze({
|
||||
path: repo.path,
|
||||
force: true,
|
||||
});
|
||||
reanalyzeSseRef.current = streamAnalyzeProgress(
|
||||
jobId,
|
||||
(p) => setReanalyzeProgress(p),
|
||||
() => {
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
reanalyzeSseRef.current = null;
|
||||
onAnalyzeComplete?.(repo.name);
|
||||
},
|
||||
(errMsg) => {
|
||||
console.error('Re-analyze failed:', errMsg);
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
reanalyzeSseRef.current = null;
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to start re-analysis:', err);
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
}
|
||||
}}
|
||||
disabled={!!reanalyzing}
|
||||
className={`cursor-pointer rounded p-1 transition-all ${
|
||||
reanalyzing === repo.name
|
||||
? 'text-accent'
|
||||
: 'text-text-muted/0 group-hover:text-text-muted hover:!text-accent'
|
||||
}`}
|
||||
title={
|
||||
reanalyzing === repo.name
|
||||
? t('header:reanalyzing')
|
||||
: t('header:reanalyzeRepo', { repoName: repo.name })
|
||||
}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3.5 w-3.5 ${reanalyzing === repo.name ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{/* Delete */}
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
// Abort any running re-analysis for this repo
|
||||
if (reanalyzing === repo.name) {
|
||||
reanalyzeSseRef.current?.abort();
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
reanalyzeSseRef.current = null;
|
||||
}
|
||||
setDeleteError(null);
|
||||
try {
|
||||
await deleteRepo(repo.name);
|
||||
const updated = await fetchRepos();
|
||||
onReposChanged?.(updated);
|
||||
// If we deleted the active repo, switch to first available
|
||||
if (repo.name === projectName && updated.length > 0) {
|
||||
onSwitchRepo?.(updated[0].name);
|
||||
} else if (updated.length === 0) {
|
||||
// No repos left — go back to onboarding
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
// Surface the failure instead of silently no-opping —
|
||||
// e.g. an origin-blocked 403 when driving a local
|
||||
// backend from the hosted UI.
|
||||
console.error('Failed to delete repo:', err);
|
||||
setDeleteError(formatBackendError(err, t));
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer rounded p-1 text-text-muted/0 transition-all group-hover:text-text-muted hover:!text-red-400"
|
||||
title={t('header:deleteRepo', { repoName: repo.name })}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div className="shrink-0 px-3 pb-2">
|
||||
<div className="flex items-center gap-2 rounded-md border border-border-subtle bg-deep/70 px-2.5 py-1.5 transition-colors focus-within:border-accent/60">
|
||||
<Search className="h-3.5 w-3.5 shrink-0 text-text-muted" />
|
||||
<input
|
||||
type="text"
|
||||
aria-label={t('header:searchRepositories')}
|
||||
placeholder={t('header:searchRepositories')}
|
||||
value={repoSearchQuery}
|
||||
onChange={(e) => setRepoSearchQuery(e.target.value)}
|
||||
className="min-w-0 flex-1 border-none bg-transparent text-xs text-text-primary outline-none placeholder:text-text-muted"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto pb-1">
|
||||
{filteredRepos.length === 0 ? (
|
||||
<div className="px-4 py-3 text-sm text-text-muted">
|
||||
{t('header:noRepositoriesFound', { query: repoSearchQuery })}
|
||||
</div>
|
||||
) : (
|
||||
filteredRepos.map((repo) => (
|
||||
<div
|
||||
key={repo.path || repo.name}
|
||||
className={`group flex items-center gap-2 px-4 py-2 transition-colors ${
|
||||
repo.name === projectName
|
||||
? 'border-l-2 border-accent bg-accent/10'
|
||||
: 'hover:bg-hover'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (repo.name !== projectName) onSwitchRepo?.(repo.name);
|
||||
setIsRepoDropdownOpen(false);
|
||||
setRepoSearchQuery('');
|
||||
}}
|
||||
className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 text-left"
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5 shrink-0 text-node-folder" />
|
||||
<span className="flex-1 truncate font-mono text-sm text-text-primary">
|
||||
{repo.name}
|
||||
</span>
|
||||
{repo.name === projectName && (
|
||||
<span className="shrink-0 font-mono text-[10px] text-accent">
|
||||
{t('header:active')}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{/* Re-analyze */}
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (reanalyzing) return; // already running
|
||||
setReanalyzing(repo.name);
|
||||
setReanalyzeProgress({
|
||||
phase: 'queued',
|
||||
percent: 0,
|
||||
message: t('common:progress.starting'),
|
||||
});
|
||||
try {
|
||||
const { jobId } = await startAnalyze({
|
||||
path: repo.path,
|
||||
force: true,
|
||||
});
|
||||
reanalyzeSseRef.current = streamAnalyzeProgress(
|
||||
jobId,
|
||||
(p) => setReanalyzeProgress(p),
|
||||
() => {
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
reanalyzeSseRef.current = null;
|
||||
onAnalyzeComplete?.(repo.name);
|
||||
},
|
||||
(errMsg) => {
|
||||
console.error('Re-analyze failed:', errMsg);
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
reanalyzeSseRef.current = null;
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to start re-analysis:', err);
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
}
|
||||
}}
|
||||
disabled={!!reanalyzing}
|
||||
className={`cursor-pointer rounded p-1 transition-all ${
|
||||
reanalyzing === repo.name
|
||||
? 'text-accent'
|
||||
: 'text-text-muted/0 group-hover:text-text-muted hover:!text-accent'
|
||||
}`}
|
||||
title={
|
||||
reanalyzing === repo.name
|
||||
? t('header:reanalyzing')
|
||||
: t('header:reanalyzeRepo', { repoName: repo.name })
|
||||
}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-3.5 w-3.5 ${reanalyzing === repo.name ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{/* Delete */}
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
// Abort any running re-analysis for this repo
|
||||
if (reanalyzing === repo.name) {
|
||||
reanalyzeSseRef.current?.abort();
|
||||
setReanalyzing(null);
|
||||
setReanalyzeProgress(null);
|
||||
reanalyzeSseRef.current = null;
|
||||
}
|
||||
setDeleteError(null);
|
||||
try {
|
||||
await deleteRepo(repo.name);
|
||||
const updated = await fetchRepos();
|
||||
onReposChanged?.(updated);
|
||||
// If we deleted the active repo, switch to first available
|
||||
if (repo.name === projectName && updated.length > 0) {
|
||||
onSwitchRepo?.(updated[0].name);
|
||||
} else if (updated.length === 0) {
|
||||
// No repos left — go back to onboarding
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
// Surface the failure instead of silently no-opping —
|
||||
// e.g. an origin-blocked 403 when driving a local
|
||||
// backend from the hosted UI.
|
||||
console.error('Failed to delete repo:', err);
|
||||
setDeleteError(formatBackendError(err, t));
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer rounded p-1 text-text-muted/0 transition-all group-hover:text-text-muted hover:!text-red-400"
|
||||
title={t('header:deleteRepo', { repoName: repo.name })}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -375,7 +409,10 @@ export const Header = ({
|
|||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowAnalyzer(true)}
|
||||
onClick={() => {
|
||||
setRepoSearchQuery('');
|
||||
setShowAnalyzer(true);
|
||||
}}
|
||||
disabled={!!reanalyzing}
|
||||
className="flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
|
|
@ -385,7 +422,7 @@ export const Header = ({
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
"deleteRepo": "Delete {{repoName}}",
|
||||
"reanalyzingRepo": "Re-analyzing {{repoName}}: {{message}}",
|
||||
"analyzeNew": "Analyze a new repository...",
|
||||
"searchRepositories": "Search repositories...",
|
||||
"noRepositoriesFound": "No repositories found for \"{{query}}\"",
|
||||
"searchNodes": "Search nodes...",
|
||||
"noNodesFound": "No nodes found for \"{{query}}\"",
|
||||
"starIfCool": "Star if cool",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
"deleteRepo": "删除 {{repoName}}",
|
||||
"reanalyzingRepo": "正在重新分析 {{repoName}}:{{message}}",
|
||||
"analyzeNew": "分析新仓库...",
|
||||
"searchRepositories": "搜索仓库...",
|
||||
"noRepositoriesFound": "未找到“{{query}}”相关仓库",
|
||||
"searchNodes": "搜索节点...",
|
||||
"noNodesFound": "未找到“{{query}}”相关节点",
|
||||
"starIfCool": "觉得不错就点星",
|
||||
|
|
|
|||
155
gitnexus-web/test/unit/header.test.tsx
Normal file
155
gitnexus-web/test/unit/header.test.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Header } from '../../src/components/Header';
|
||||
import type { BackendRepo } from '../../src/services/backend-client';
|
||||
|
||||
vi.mock('../../src/hooks/useAppState', () => ({
|
||||
useAppState: () => ({
|
||||
projectName: 'reels',
|
||||
graph: null,
|
||||
graphMode: 'full',
|
||||
openChatPanel: vi.fn(),
|
||||
isRightPanelOpen: false,
|
||||
rightPanelTab: 'chat',
|
||||
setSettingsPanelOpen: vi.fn(),
|
||||
setHelpDialogBoxOpen: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/components/EmbeddingStatus', () => ({
|
||||
EmbeddingStatus: () => <div data-testid="embedding-status" />,
|
||||
}));
|
||||
|
||||
vi.mock('../../src/components/LanguageSwitcher', () => ({
|
||||
LanguageSwitcher: () => <div data-testid="language-switcher" />,
|
||||
}));
|
||||
|
||||
vi.mock('../../src/components/RepoAnalyzer', () => ({
|
||||
RepoAnalyzer: () => <div data-testid="repo-analyzer" />,
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/backend-client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/services/backend-client')>();
|
||||
return {
|
||||
...actual,
|
||||
deleteRepo: vi.fn(),
|
||||
fetchRepos: vi.fn(),
|
||||
startAnalyze: vi.fn(),
|
||||
streamAnalyzeProgress: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (key === 'header:repositories') return 'Repositories';
|
||||
if (key === 'header:active') return 'Active';
|
||||
if (key === 'header:reanalyzeRepo') return `Re-analyze ${options?.repoName ?? ''}`;
|
||||
if (key === 'header:deleteRepo') return `Delete ${options?.repoName ?? ''}`;
|
||||
if (key === 'header:analyzeNew') return 'Analyze new';
|
||||
if (key === 'header:searchRepositories') return 'Search repositories...';
|
||||
if (key === 'header:noRepositoriesFound')
|
||||
return `No repositories found for ${options?.query}`;
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
function makeRepo(index: number): BackendRepo {
|
||||
return {
|
||||
name: index === 0 ? 'reels' : `repo-${index}`,
|
||||
path: `/tmp/repo-${index}`,
|
||||
stats: {
|
||||
files: 1,
|
||||
nodes: 1,
|
||||
edges: 0,
|
||||
communities: 0,
|
||||
processes: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Header', () => {
|
||||
it('keeps a large repository menu scrollable inside the viewport', () => {
|
||||
render(<Header availableRepos={Array.from({ length: 30 }, (_, index) => makeRepo(index))} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /reels/i }));
|
||||
|
||||
const menu = screen.getByText('Repositories').closest('.absolute');
|
||||
expect(menu).not.toBeNull();
|
||||
expect(menu).toHaveClass('max-h-[calc(100vh-4.5rem)]');
|
||||
expect(menu).toHaveClass('overflow-hidden');
|
||||
|
||||
const scrollableRepoList = screen.getByText('repo-29').closest('.scrollbar-thin');
|
||||
expect(scrollableRepoList).not.toBeNull();
|
||||
expect(scrollableRepoList).toHaveClass('overflow-y-auto');
|
||||
expect(scrollableRepoList).toHaveClass('flex-1');
|
||||
});
|
||||
|
||||
it('filters repositories locally by displayed name', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Header
|
||||
availableRepos={[
|
||||
{ ...makeRepo(0), name: 'reels', path: '/workspace/apps/reels' },
|
||||
{ ...makeRepo(1), name: 'gitnexus-web', path: '/workspace/GitNexus/gitnexus-web' },
|
||||
{ ...makeRepo(2), name: 'api-server', path: '/workspace/gitnexus/api' },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /reels/i }));
|
||||
|
||||
const input = screen.getByRole('textbox', { name: 'Search repositories...' });
|
||||
await user.type(input, 'gitnexus');
|
||||
|
||||
expect(screen.getByText('gitnexus-web')).toBeInTheDocument();
|
||||
expect(screen.queryByText('api-server')).not.toBeInTheDocument();
|
||||
|
||||
await user.clear(input);
|
||||
await user.type(input, 'api');
|
||||
|
||||
expect(screen.getByText('api-server')).toBeInTheDocument();
|
||||
expect(screen.queryByText('gitnexus-web')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an empty state when no repositories match the local search', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Header availableRepos={Array.from({ length: 3 }, (_, index) => makeRepo(index))} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /reels/i }));
|
||||
|
||||
await user.type(screen.getByRole('textbox', { name: 'Search repositories...' }), 'missing');
|
||||
|
||||
expect(screen.getByText('No repositories found for missing')).toBeInTheDocument();
|
||||
expect(screen.queryByText('repo-1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not leave stale rows when duplicate repository names are filtered', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Header
|
||||
availableRepos={[
|
||||
{ ...makeRepo(0), name: 'search_sync', path: '/workspace/group-a/search_sync' },
|
||||
{ ...makeRepo(1), name: 'tab_server', path: '/workspace/group-a/tab_server' },
|
||||
{ ...makeRepo(2), name: 'feed_sync', path: '/workspace/group-a/feed_sync' },
|
||||
{ ...makeRepo(3), name: 'search_sync', path: '/workspace/group-b/search_sync' },
|
||||
{ ...makeRepo(4), name: 'tab_server', path: '/workspace/group-b/tab_server' },
|
||||
{ ...makeRepo(5), name: 'reels', path: '/workspace/group-b/reels' },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /reels/i }));
|
||||
|
||||
await user.type(screen.getByRole('textbox', { name: 'Search repositories...' }), 'tab');
|
||||
|
||||
const repoList = screen.getAllByText('tab_server')[0].closest('.scrollbar-thin');
|
||||
expect(repoList).not.toBeNull();
|
||||
expect(repoList).toHaveTextContent('tab_server');
|
||||
expect(repoList).not.toHaveTextContent('search_sync');
|
||||
expect(repoList).not.toHaveTextContent('feed_sync');
|
||||
expect(repoList).not.toHaveTextContent('reels');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue