fix(extensions): honor installed MCP targets in manage actions
This commit is contained in:
parent
acabe52ae7
commit
0d6276ea0b
5 changed files with 453 additions and 10 deletions
|
|
@ -18,11 +18,16 @@ import { Github as GithubIcon } from 'lucide-react';
|
|||
import { InstallButton } from '../common/InstallButton';
|
||||
import { SourceBadge } from '../common/SourceBadge';
|
||||
|
||||
import type { McpCatalogItem, McpServerDiagnostic } from '@shared/types/extensions';
|
||||
import type {
|
||||
InstalledMcpEntry,
|
||||
McpCatalogItem,
|
||||
McpServerDiagnostic,
|
||||
} from '@shared/types/extensions';
|
||||
|
||||
interface McpServerCardProps {
|
||||
server: McpCatalogItem;
|
||||
isInstalled: boolean;
|
||||
installedEntry?: InstalledMcpEntry | null;
|
||||
diagnostic?: McpServerDiagnostic | null;
|
||||
diagnosticsLoading?: boolean;
|
||||
onClick: (serverId: string) => void;
|
||||
|
|
@ -31,6 +36,7 @@ interface McpServerCardProps {
|
|||
export const McpServerCard = ({
|
||||
server,
|
||||
isInstalled,
|
||||
installedEntry,
|
||||
diagnostic,
|
||||
diagnosticsLoading,
|
||||
onClick,
|
||||
|
|
@ -48,6 +54,14 @@ export const McpServerCard = ({
|
|||
server.envVars.length > 0 ||
|
||||
server.requiresAuth ||
|
||||
(server.authHeaders?.length ?? 0) > 0;
|
||||
const defaultServerName = sanitizeMcpServerName(server.name);
|
||||
const supportsDirectInstalledAction =
|
||||
isInstalled &&
|
||||
installedEntry?.scope === 'user' &&
|
||||
installedEntry.name === defaultServerName &&
|
||||
!requiresConfiguration;
|
||||
const shouldShowDirectInstallButton =
|
||||
canAutoInstall && (!isInstalled ? !requiresConfiguration : supportsDirectInstalledAction);
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const hasIcon = !!server.iconUrl && !imgError;
|
||||
const diagnosticBadgeClass =
|
||||
|
|
@ -224,7 +238,7 @@ export const McpServerCard = ({
|
|||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{canAutoInstall && !requiresConfiguration && (
|
||||
{shouldShowDirectInstallButton && (
|
||||
<div className="shrink-0">
|
||||
<InstallButton
|
||||
state={installProgress}
|
||||
|
|
@ -232,19 +246,21 @@ export const McpServerCard = ({
|
|||
onInstall={() =>
|
||||
installMcpServer({
|
||||
registryId: server.id,
|
||||
serverName: sanitizeMcpServerName(server.name),
|
||||
serverName: defaultServerName,
|
||||
scope: 'user',
|
||||
envValues: {},
|
||||
headers: [],
|
||||
})
|
||||
}
|
||||
onUninstall={() => uninstallMcpServer(server.id, sanitizeMcpServerName(server.name))}
|
||||
onUninstall={() =>
|
||||
uninstallMcpServer(server.id, installedEntry?.name ?? defaultServerName, 'user')
|
||||
}
|
||||
size="sm"
|
||||
errorMessage={installError}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{canAutoInstall && requiresConfiguration && (
|
||||
{canAutoInstall && (!shouldShowDirectInstallButton || requiresConfiguration) && (
|
||||
<div className="shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -31,11 +31,17 @@ import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react';
|
|||
import { InstallButton } from '../common/InstallButton';
|
||||
import { SourceBadge } from '../common/SourceBadge';
|
||||
|
||||
import type { McpCatalogItem, McpHeaderDef, McpServerDiagnostic } from '@shared/types/extensions';
|
||||
import type {
|
||||
InstalledMcpEntry,
|
||||
McpCatalogItem,
|
||||
McpHeaderDef,
|
||||
McpServerDiagnostic,
|
||||
} from '@shared/types/extensions';
|
||||
|
||||
interface McpServerDetailDialogProps {
|
||||
server: McpCatalogItem | null;
|
||||
isInstalled: boolean;
|
||||
installedEntry?: InstalledMcpEntry | null;
|
||||
diagnostic?: McpServerDiagnostic | null;
|
||||
diagnosticsLoading?: boolean;
|
||||
open: boolean;
|
||||
|
|
@ -52,6 +58,7 @@ const SCOPE_OPTIONS: { value: Scope; label: string }[] = [
|
|||
export const McpServerDetailDialog = ({
|
||||
server,
|
||||
isInstalled,
|
||||
installedEntry,
|
||||
diagnostic,
|
||||
diagnosticsLoading,
|
||||
open,
|
||||
|
|
@ -80,7 +87,6 @@ export const McpServerDetailDialog = ({
|
|||
return;
|
||||
}
|
||||
|
||||
setServerName(sanitizeMcpServerName(server.name));
|
||||
setEnvValues(Object.fromEntries(server.envVars.map((env) => [env.name, ''])));
|
||||
setHeaders(
|
||||
(server.authHeaders ?? []).map((header) => ({
|
||||
|
|
@ -93,10 +99,11 @@ export const McpServerDetailDialog = ({
|
|||
locked: true,
|
||||
}))
|
||||
);
|
||||
setScope('user');
|
||||
setServerName(installedEntry?.name ?? sanitizeMcpServerName(server.name));
|
||||
setScope(installedEntry?.scope === 'local' ? 'local' : 'user');
|
||||
setImgError(false);
|
||||
setAutoFilledFields(new Set());
|
||||
}, [server?.id, open]);
|
||||
}, [installedEntry?.name, installedEntry?.scope, open, server?.id]);
|
||||
|
||||
// Auto-fill env values from saved API keys
|
||||
useEffect(() => {
|
||||
|
|
@ -170,6 +177,8 @@ export const McpServerDetailDialog = ({
|
|||
(header) => header.isRequired && !header.value.trim()
|
||||
);
|
||||
const installDisabled = !serverName.trim() || missingRequiredEnvVars || missingRequiredHeaders;
|
||||
const uninstallServerName = installedEntry?.name ?? serverName;
|
||||
const uninstallScope = installedEntry?.scope ?? scope;
|
||||
const diagnosticBadgeClass =
|
||||
diagnostic?.status === 'connected'
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400'
|
||||
|
|
@ -190,7 +199,7 @@ export const McpServerDetailDialog = ({
|
|||
};
|
||||
|
||||
const handleUninstall = () => {
|
||||
uninstallMcpServer(server.id, serverName, scope);
|
||||
uninstallMcpServer(server.id, uninstallServerName, uninstallScope);
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
|
|
@ -380,6 +389,7 @@ export const McpServerDetailDialog = ({
|
|||
onChange={(e) => setServerName(e.target.value)}
|
||||
placeholder="my-server"
|
||||
className="h-8 text-sm"
|
||||
disabled={isInstalled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -374,6 +374,7 @@ export const McpServersPanel = ({
|
|||
key={server.id}
|
||||
server={server}
|
||||
isInstalled={isServerInstalled(server)}
|
||||
installedEntry={getInstalledEntry(server)}
|
||||
diagnostic={getDiagnostic(server)}
|
||||
diagnosticsLoading={mcpDiagnosticsLoading}
|
||||
onClick={setSelectedMcpServerId}
|
||||
|
|
@ -400,6 +401,7 @@ export const McpServersPanel = ({
|
|||
<McpServerDetailDialog
|
||||
server={selectedServer}
|
||||
isInstalled={selectedServer ? isServerInstalled(selectedServer) : false}
|
||||
installedEntry={selectedServer ? getInstalledEntry(selectedServer) : null}
|
||||
diagnostic={selectedServer ? getDiagnostic(selectedServer) : null}
|
||||
diagnosticsLoading={mcpDiagnosticsLoading}
|
||||
open={selectedMcpServerId !== null}
|
||||
|
|
|
|||
193
test/renderer/components/extensions/mcp/McpServerCard.test.ts
Normal file
193
test/renderer/components/extensions/mcp/McpServerCard.test.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { InstalledMcpEntry, McpCatalogItem } from '@shared/types/extensions';
|
||||
|
||||
interface StoreState {
|
||||
mcpInstallProgress: Record<string, string>;
|
||||
installMcpServer: ReturnType<typeof vi.fn>;
|
||||
uninstallMcpServer: ReturnType<typeof vi.fn>;
|
||||
installErrors: Record<string, string>;
|
||||
mcpGitHubStars: Record<string, number>;
|
||||
}
|
||||
|
||||
const storeState = {} as StoreState;
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/api', () => ({
|
||||
api: {
|
||||
openExternal: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/badge', () => ({
|
||||
Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
type = 'button',
|
||||
}: React.PropsWithChildren<{
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
}>) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{
|
||||
type,
|
||||
onClick,
|
||||
},
|
||||
children
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: React.PropsWithChildren) => React.createElement(React.Fragment, null, children),
|
||||
TooltipTrigger: ({ children }: React.PropsWithChildren) =>
|
||||
React.createElement(React.Fragment, null, children),
|
||||
TooltipContent: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/extensions/common/InstallButton', () => ({
|
||||
InstallButton: () => React.createElement('button', { type: 'button', 'data-testid': 'install-button' }, 'Install'),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/extensions/common/SourceBadge', () => ({
|
||||
SourceBadge: ({ source }: { source: string }) => React.createElement('span', null, source),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/utils/formatters', () => ({
|
||||
formatCompactNumber: (value: number) => String(value),
|
||||
formatRelativeTime: () => 'recently',
|
||||
}));
|
||||
|
||||
vi.mock('lucide-react', () => {
|
||||
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
|
||||
return {
|
||||
Clock: Icon,
|
||||
Cloud: Icon,
|
||||
Globe: Icon,
|
||||
KeyRound: Icon,
|
||||
Lock: Icon,
|
||||
Monitor: Icon,
|
||||
Star: Icon,
|
||||
Tag: Icon,
|
||||
Wrench: Icon,
|
||||
Github: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
import { McpServerCard } from '@renderer/components/extensions/mcp/McpServerCard';
|
||||
|
||||
function makeServer(): McpCatalogItem {
|
||||
return {
|
||||
id: 'io.github.upstash/context7',
|
||||
name: 'Context7',
|
||||
description: 'Docs server',
|
||||
source: 'official',
|
||||
installSpec: {
|
||||
type: 'stdio',
|
||||
npmPackage: '@upstash/context7-mcp',
|
||||
},
|
||||
envVars: [],
|
||||
tools: [],
|
||||
requiresAuth: false,
|
||||
authHeaders: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('McpServerCard direct action safety', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.mcpInstallProgress = {};
|
||||
storeState.installMcpServer = vi.fn();
|
||||
storeState.uninstallMcpServer = vi.fn();
|
||||
storeState.installErrors = {};
|
||||
storeState.mcpGitHubStars = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('falls back to Manage for installed entries that cannot be safely uninstalled directly', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const onClick = vi.fn();
|
||||
const installedEntry: InstalledMcpEntry = {
|
||||
name: 'context7-local',
|
||||
scope: 'local',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(McpServerCard, {
|
||||
server: makeServer(),
|
||||
isInstalled: true,
|
||||
installedEntry,
|
||||
diagnostic: null,
|
||||
diagnosticsLoading: false,
|
||||
onClick,
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.querySelector('[data-testid="install-button"]')).toBeNull();
|
||||
const manageButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent === 'Manage'
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(manageButton).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
manageButton?.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onClick).toHaveBeenCalledWith('io.github.upstash/context7');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps direct actions for standard user-scope installs', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const installedEntry: InstalledMcpEntry = {
|
||||
name: 'context7',
|
||||
scope: 'user',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(McpServerCard, {
|
||||
server: makeServer(),
|
||||
isInstalled: true,
|
||||
installedEntry,
|
||||
diagnostic: null,
|
||||
diagnosticsLoading: false,
|
||||
onClick: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.querySelector('[data-testid="install-button"]')).not.toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { InstalledMcpEntry, McpCatalogItem } from '@shared/types/extensions';
|
||||
|
||||
interface StoreState {
|
||||
mcpInstallProgress: Record<string, string>;
|
||||
installMcpServer: ReturnType<typeof vi.fn>;
|
||||
uninstallMcpServer: ReturnType<typeof vi.fn>;
|
||||
installErrors: Record<string, string>;
|
||||
mcpGitHubStars: Record<string, number>;
|
||||
}
|
||||
|
||||
const storeState = {} as StoreState;
|
||||
const lookupMock = vi.fn();
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/api', () => ({
|
||||
api: {
|
||||
openExternal: vi.fn(),
|
||||
apiKeys: {
|
||||
lookup: (...args: unknown[]) => lookupMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/badge', () => ({
|
||||
Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/button', () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
type = 'button',
|
||||
disabled,
|
||||
}: React.PropsWithChildren<{
|
||||
onClick?: () => void;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
disabled?: boolean;
|
||||
}>) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{
|
||||
type,
|
||||
disabled,
|
||||
onClick,
|
||||
},
|
||||
children
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/dialog', () => ({
|
||||
Dialog: ({ open, children }: React.PropsWithChildren<{ open: boolean }>) =>
|
||||
open ? React.createElement('div', null, children) : null,
|
||||
DialogContent: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
|
||||
DialogHeader: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
|
||||
DialogTitle: ({ children }: React.PropsWithChildren) => React.createElement('h2', null, children),
|
||||
DialogDescription: ({ children }: React.PropsWithChildren) =>
|
||||
React.createElement('p', null, children),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/input', () => ({
|
||||
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) =>
|
||||
React.createElement('input', props),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/label', () => ({
|
||||
Label: ({ children }: React.PropsWithChildren) => React.createElement('label', null, children),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/ui/select', () => ({
|
||||
Select: ({
|
||||
children,
|
||||
value,
|
||||
onValueChange,
|
||||
}: React.PropsWithChildren<{ value: string; onValueChange: (value: string) => void }>) =>
|
||||
React.createElement(
|
||||
'select',
|
||||
{
|
||||
'data-testid': 'scope-select',
|
||||
value,
|
||||
onChange: (event: React.ChangeEvent<HTMLSelectElement>) => onValueChange(event.target.value),
|
||||
},
|
||||
children
|
||||
),
|
||||
SelectTrigger: ({ children }: React.PropsWithChildren) =>
|
||||
React.createElement(React.Fragment, null, children),
|
||||
SelectValue: () => null,
|
||||
SelectContent: ({ children }: React.PropsWithChildren) =>
|
||||
React.createElement(React.Fragment, null, children),
|
||||
SelectItem: ({ children, value }: React.PropsWithChildren<{ value: string }>) =>
|
||||
React.createElement('option', { value }, children),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/extensions/common/InstallButton', () => ({
|
||||
InstallButton: ({
|
||||
isInstalled,
|
||||
onInstall,
|
||||
onUninstall,
|
||||
}: {
|
||||
isInstalled: boolean;
|
||||
onInstall: () => void;
|
||||
onUninstall: () => void;
|
||||
}) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
'data-testid': 'install-button',
|
||||
onClick: () => (isInstalled ? onUninstall() : onInstall()),
|
||||
},
|
||||
isInstalled ? 'Uninstall' : 'Install'
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@renderer/components/extensions/common/SourceBadge', () => ({
|
||||
SourceBadge: ({ source }: { source: string }) => React.createElement('span', null, source),
|
||||
}));
|
||||
|
||||
vi.mock('lucide-react', () => {
|
||||
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
|
||||
return {
|
||||
ExternalLink: Icon,
|
||||
Lock: Icon,
|
||||
Plus: Icon,
|
||||
Star: Icon,
|
||||
Trash2: Icon,
|
||||
Wrench: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
import { McpServerDetailDialog } from '@renderer/components/extensions/mcp/McpServerDetailDialog';
|
||||
|
||||
function makeServer(): McpCatalogItem {
|
||||
return {
|
||||
id: 'io.github.upstash/context7',
|
||||
name: 'Context7',
|
||||
description: 'Docs server',
|
||||
source: 'official',
|
||||
installSpec: {
|
||||
type: 'stdio',
|
||||
npmPackage: '@upstash/context7-mcp',
|
||||
},
|
||||
envVars: [],
|
||||
tools: [],
|
||||
requiresAuth: false,
|
||||
authHeaders: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe('McpServerDetailDialog installed entry handling', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.mcpInstallProgress = {};
|
||||
storeState.installMcpServer = vi.fn();
|
||||
storeState.uninstallMcpServer = vi.fn();
|
||||
storeState.installErrors = {};
|
||||
storeState.mcpGitHubStars = {};
|
||||
lookupMock.mockReset();
|
||||
lookupMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('uninstalls using the real installed server name and scope', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const installedEntry: InstalledMcpEntry = {
|
||||
name: 'context7-local',
|
||||
scope: 'local',
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(McpServerDetailDialog, {
|
||||
server: makeServer(),
|
||||
isInstalled: true,
|
||||
installedEntry,
|
||||
diagnostic: null,
|
||||
diagnosticsLoading: false,
|
||||
open: true,
|
||||
onClose: vi.fn(),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const serverNameInput = host.querySelector('#server-name') as HTMLInputElement;
|
||||
expect(serverNameInput).not.toBeNull();
|
||||
expect(serverNameInput.value).toBe('context7-local');
|
||||
expect(serverNameInput.disabled).toBe(true);
|
||||
|
||||
const scopeSelect = host.querySelector('[data-testid="scope-select"]') as HTMLSelectElement;
|
||||
expect(scopeSelect.value).toBe('local');
|
||||
|
||||
const uninstallButton = host.querySelector('[data-testid="install-button"]') as HTMLButtonElement;
|
||||
await act(async () => {
|
||||
uninstallButton.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(storeState.uninstallMcpServer).toHaveBeenCalledWith(
|
||||
'io.github.upstash/context7',
|
||||
'context7-local',
|
||||
'local'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue