666 lines
20 KiB
TypeScript
666 lines
20 KiB
TypeScript
import React, { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
|
|
import type { CliInstallationStatus } from '@shared/types';
|
|
import type { SkillCatalogItem } from '@shared/types/extensions';
|
|
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
|
|
|
interface StoreState {
|
|
fetchSkillsCatalog: ReturnType<typeof vi.fn>;
|
|
fetchSkillDetail: ReturnType<typeof vi.fn>;
|
|
skillsCatalogLoadingByProjectPath: Record<string, boolean>;
|
|
skillsCatalogErrorByProjectPath: Record<string, string | null>;
|
|
skillsDetailsById: Record<string, unknown>;
|
|
skillsUserCatalog: SkillCatalogItem[];
|
|
skillsProjectCatalogByProjectPath: Record<string, SkillCatalogItem[]>;
|
|
cliStatus: CliInstallationStatus | null;
|
|
cliStatusLoading: boolean;
|
|
appConfig: {
|
|
general: {
|
|
multimodelEnabled: boolean;
|
|
};
|
|
} | null;
|
|
}
|
|
|
|
const storeState = {} as StoreState;
|
|
const startWatchingMock = vi.fn();
|
|
const stopWatchingMock = vi.fn();
|
|
const onChangedMock = vi.fn();
|
|
const codexAccountHookState = {
|
|
snapshot: null as CodexAccountSnapshotDto | null,
|
|
loading: false,
|
|
error: null as string | null,
|
|
refresh: vi.fn(() => Promise.resolve(undefined)),
|
|
startChatgptLogin: vi.fn(() => Promise.resolve(true)),
|
|
cancelChatgptLogin: vi.fn(() => Promise.resolve(true)),
|
|
logout: vi.fn(() => Promise.resolve(true)),
|
|
};
|
|
let skillsChangedHandler: ((event: {
|
|
scope: 'user' | 'project';
|
|
projectPath: string | null;
|
|
path: string;
|
|
type: 'create' | 'change' | 'delete';
|
|
}) => void) | null = null;
|
|
|
|
vi.mock('@renderer/store', () => ({
|
|
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
|
}));
|
|
|
|
vi.mock('@features/codex-account/renderer', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@features/codex-account/renderer')>();
|
|
return {
|
|
...actual,
|
|
useCodexAccountSnapshot: () => codexAccountHookState,
|
|
};
|
|
});
|
|
|
|
vi.mock('zustand/react/shallow', () => ({
|
|
useShallow: <T,>(selector: T) => selector,
|
|
}));
|
|
|
|
vi.mock('@renderer/api', () => ({
|
|
api: {
|
|
skills: {
|
|
startWatching: (...args: unknown[]) => startWatchingMock(...args),
|
|
stopWatching: (...args: unknown[]) => stopWatchingMock(...args),
|
|
onChanged: (...args: unknown[]) => onChangedMock(...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',
|
|
}: React.PropsWithChildren<{
|
|
onClick?: () => void;
|
|
type?: 'button' | 'submit' | 'reset';
|
|
variant?: string;
|
|
size?: string;
|
|
className?: string;
|
|
}>) =>
|
|
React.createElement(
|
|
'button',
|
|
{
|
|
type,
|
|
onClick,
|
|
},
|
|
children
|
|
),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/popover', () => ({
|
|
Popover: ({ children }: React.PropsWithChildren<{ open?: boolean; onOpenChange?: (open: boolean) => void }>) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
PopoverTrigger: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
PopoverContent: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement('div', null, 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/SearchInput', () => ({
|
|
SearchInput: ({
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
}: {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
}) =>
|
|
React.createElement('input', {
|
|
value,
|
|
placeholder,
|
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value),
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/skills/SkillDetailDialog', () => ({
|
|
SkillDetailDialog: () => null,
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/skills/SkillEditorDialog', () => ({
|
|
SkillEditorDialog: ({ allowCodexRootKind }: { allowCodexRootKind: boolean }) =>
|
|
React.createElement('div', {
|
|
'data-testid': 'skill-editor-dialog',
|
|
'data-allow-codex-root-kind': String(allowCodexRootKind),
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/skills/SkillImportDialog', () => ({
|
|
SkillImportDialog: ({ allowCodexRootKind }: { allowCodexRootKind: boolean }) =>
|
|
React.createElement('div', {
|
|
'data-testid': 'skill-import-dialog',
|
|
'data-allow-codex-root-kind': String(allowCodexRootKind),
|
|
}),
|
|
}));
|
|
|
|
vi.mock('lucide-react', () => {
|
|
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
|
|
return {
|
|
AlertTriangle: Icon,
|
|
ArrowUpAZ: Icon,
|
|
ArrowUpDown: Icon,
|
|
BookOpen: Icon,
|
|
Check: Icon,
|
|
CheckCircle2: Icon,
|
|
Clock3: Icon,
|
|
Download: Icon,
|
|
Plus: Icon,
|
|
Search: Icon,
|
|
};
|
|
});
|
|
|
|
import { SkillsPanel } from '@renderer/components/extensions/skills/SkillsPanel';
|
|
|
|
function makeUserSkill(): SkillCatalogItem {
|
|
return {
|
|
id: '/Users/me/.claude/skills/review-helper',
|
|
sourceType: 'filesystem',
|
|
name: 'Review Helper',
|
|
description: 'Helps with code review',
|
|
folderName: 'review-helper',
|
|
scope: 'user',
|
|
rootKind: 'claude',
|
|
projectRoot: null,
|
|
discoveryRoot: '/Users/me/.claude/skills',
|
|
skillDir: '/Users/me/.claude/skills/review-helper',
|
|
skillFile: '/Users/me/.claude/skills/review-helper/SKILL.md',
|
|
metadata: {},
|
|
invocationMode: 'auto',
|
|
flags: {
|
|
hasScripts: false,
|
|
hasReferences: false,
|
|
hasAssets: false,
|
|
},
|
|
isValid: true,
|
|
issues: [],
|
|
modifiedAt: 1,
|
|
};
|
|
}
|
|
|
|
function makeCodexSkill(): SkillCatalogItem {
|
|
return {
|
|
...makeUserSkill(),
|
|
id: '/Users/me/.codex/skills/codex-helper',
|
|
name: 'Codex Helper',
|
|
description: 'Helps only Codex sessions',
|
|
folderName: 'codex-helper',
|
|
rootKind: 'codex',
|
|
discoveryRoot: '/Users/me/.codex/skills',
|
|
skillDir: '/Users/me/.codex/skills/codex-helper',
|
|
skillFile: '/Users/me/.codex/skills/codex-helper/SKILL.md',
|
|
};
|
|
}
|
|
|
|
function makeMultimodelStatus(
|
|
overrides?: Partial<CliInstallationStatus>
|
|
): CliInstallationStatus {
|
|
return {
|
|
flavor: 'agent_teams_orchestrator',
|
|
displayName: 'Multimodel runtime',
|
|
supportsSelfUpdate: false,
|
|
showVersionDetails: true,
|
|
showBinaryPath: true,
|
|
installed: true,
|
|
installedVersion: '1.0.0',
|
|
binaryPath: '/usr/local/bin/agent-teams',
|
|
latestVersion: '1.0.0',
|
|
updateAvailable: false,
|
|
authLoggedIn: false,
|
|
authStatusChecking: false,
|
|
authMethod: null,
|
|
providers: [
|
|
{
|
|
providerId: 'anthropic',
|
|
displayName: 'Anthropic',
|
|
supported: true,
|
|
authenticated: true,
|
|
authMethod: 'oauth',
|
|
verificationState: 'verified',
|
|
statusMessage: 'Connected',
|
|
models: [],
|
|
canLoginFromUi: true,
|
|
capabilities: {
|
|
teamLaunch: true,
|
|
oneShot: true,
|
|
extensions: createDefaultCliExtensionCapabilities({
|
|
plugins: { status: 'supported', ownership: 'provider-scoped', reason: null },
|
|
}),
|
|
},
|
|
connection: null,
|
|
backend: null,
|
|
},
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('SkillsPanel', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
storeState.fetchSkillsCatalog = vi.fn().mockResolvedValue(undefined);
|
|
storeState.fetchSkillDetail = vi.fn().mockResolvedValue(undefined);
|
|
storeState.skillsCatalogLoadingByProjectPath = {};
|
|
storeState.skillsCatalogErrorByProjectPath = {};
|
|
storeState.skillsDetailsById = {};
|
|
storeState.skillsUserCatalog = [makeUserSkill()];
|
|
storeState.skillsProjectCatalogByProjectPath = {
|
|
'/tmp/project-a': [],
|
|
};
|
|
storeState.cliStatusLoading = false;
|
|
storeState.appConfig = {
|
|
general: {
|
|
multimodelEnabled: true,
|
|
},
|
|
};
|
|
storeState.cliStatus = {
|
|
flavor: 'claude',
|
|
displayName: 'Claude CLI',
|
|
supportsSelfUpdate: true,
|
|
showVersionDetails: true,
|
|
showBinaryPath: true,
|
|
installed: true,
|
|
installedVersion: '1.0.0',
|
|
binaryPath: '/usr/local/bin/claude',
|
|
latestVersion: '1.0.0',
|
|
updateAvailable: false,
|
|
authLoggedIn: true,
|
|
authStatusChecking: false,
|
|
authMethod: 'oauth',
|
|
providers: [],
|
|
};
|
|
codexAccountHookState.snapshot = null;
|
|
codexAccountHookState.loading = false;
|
|
codexAccountHookState.error = null;
|
|
startWatchingMock.mockReset();
|
|
stopWatchingMock.mockReset();
|
|
onChangedMock.mockReset();
|
|
skillsChangedHandler = null;
|
|
startWatchingMock.mockResolvedValue('watch-1');
|
|
onChangedMock.mockImplementation((handler: typeof skillsChangedHandler) => {
|
|
skillsChangedHandler = handler;
|
|
return () => {
|
|
skillsChangedHandler = null;
|
|
};
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('refetches personal skill details without forcing the current project path', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
const skill = storeState.skillsUserCatalog[0]!;
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: skill.id,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(startWatchingMock).toHaveBeenCalledWith('/tmp/project-a');
|
|
expect(skillsChangedHandler).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
skillsChangedHandler?.({
|
|
scope: 'user',
|
|
projectPath: null,
|
|
path: `${skill.skillDir}/SKILL.md`,
|
|
type: 'change',
|
|
});
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(storeState.fetchSkillsCatalog).toHaveBeenCalledWith('/tmp/project-a');
|
|
expect(storeState.fetchSkillDetail).toHaveBeenCalledWith(skill.id, undefined);
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('hides codex-only create and import affordances when codex runtime is unavailable', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).not.toContain('Codex only');
|
|
for (const node of host.querySelectorAll('[data-testid="skill-editor-dialog"]')) {
|
|
expect(node.getAttribute('data-allow-codex-root-kind')).toBe('false');
|
|
}
|
|
const importDialog = host.querySelector('[data-testid="skill-import-dialog"]');
|
|
expect(importDialog?.getAttribute('data-allow-codex-root-kind')).toBe('false');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('uses a runtime-aware shared skills banner when codex is unavailable', async () => {
|
|
storeState.cliStatus = makeMultimodelStatus();
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain(
|
|
'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic.'
|
|
);
|
|
expect(host.textContent).not.toContain('available to both Anthropic and Codex');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('uses the live Codex snapshot to expose Codex-only skill affordances after a stale provider bootstrap', async () => {
|
|
storeState.cliStatus = makeMultimodelStatus({
|
|
providers: [
|
|
...makeMultimodelStatus().providers,
|
|
{
|
|
providerId: 'codex',
|
|
displayName: 'Codex',
|
|
supported: false,
|
|
authenticated: false,
|
|
authMethod: null,
|
|
verificationState: 'unknown',
|
|
statusMessage: 'Checking...',
|
|
models: [],
|
|
canLoginFromUi: false,
|
|
capabilities: {
|
|
teamLaunch: true,
|
|
oneShot: true,
|
|
extensions: createDefaultCliExtensionCapabilities({
|
|
plugins: { status: 'unsupported', ownership: 'provider-scoped', reason: null },
|
|
}),
|
|
},
|
|
connection: null,
|
|
backend: null,
|
|
},
|
|
],
|
|
});
|
|
codexAccountHookState.snapshot = {
|
|
preferredAuthMode: 'chatgpt',
|
|
effectiveAuthMode: 'chatgpt',
|
|
launchAllowed: true,
|
|
launchIssueMessage: null,
|
|
launchReadinessState: 'ready_chatgpt',
|
|
appServerState: 'healthy',
|
|
appServerStatusMessage: null,
|
|
managedAccount: {
|
|
type: 'chatgpt',
|
|
email: 'user@example.com',
|
|
planType: 'pro',
|
|
},
|
|
apiKey: {
|
|
available: true,
|
|
source: 'environment',
|
|
sourceLabel: 'Detected from OPENAI_API_KEY',
|
|
},
|
|
requiresOpenaiAuth: false,
|
|
login: {
|
|
status: 'idle',
|
|
error: null,
|
|
startedAt: null,
|
|
},
|
|
rateLimits: null,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain(
|
|
'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic and Codex.'
|
|
);
|
|
expect(host.textContent).toContain('Codex only');
|
|
expect(host.textContent).toContain('Use `.codex` when a skill should stay Codex-only.');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('uses the live Codex snapshot even while multimodel provider status is still loading', async () => {
|
|
storeState.cliStatus = null;
|
|
storeState.cliStatusLoading = true;
|
|
codexAccountHookState.snapshot = {
|
|
preferredAuthMode: 'chatgpt',
|
|
effectiveAuthMode: 'chatgpt',
|
|
launchAllowed: true,
|
|
launchIssueMessage: null,
|
|
launchReadinessState: 'ready_chatgpt',
|
|
appServerState: 'healthy',
|
|
appServerStatusMessage: null,
|
|
managedAccount: {
|
|
type: 'chatgpt',
|
|
email: 'user@example.com',
|
|
planType: 'pro',
|
|
},
|
|
apiKey: {
|
|
available: true,
|
|
source: 'environment',
|
|
sourceLabel: 'Detected from OPENAI_API_KEY',
|
|
},
|
|
requiresOpenaiAuth: false,
|
|
login: {
|
|
status: 'idle',
|
|
error: null,
|
|
startedAt: null,
|
|
},
|
|
rateLimits: null,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain(
|
|
'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, and OpenCode (200+ models).'
|
|
);
|
|
expect(host.textContent).toContain('Codex only');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('resets the codex-only quick filter when codex entries disappear', async () => {
|
|
storeState.cliStatus = makeMultimodelStatus({
|
|
providers: [
|
|
...makeMultimodelStatus().providers,
|
|
{
|
|
providerId: 'codex',
|
|
displayName: 'Codex',
|
|
supported: true,
|
|
authenticated: true,
|
|
authMethod: 'api_key',
|
|
verificationState: 'verified',
|
|
statusMessage: 'Connected',
|
|
models: [],
|
|
canLoginFromUi: true,
|
|
capabilities: {
|
|
teamLaunch: true,
|
|
oneShot: true,
|
|
extensions: createDefaultCliExtensionCapabilities({
|
|
plugins: { status: 'unsupported', ownership: 'provider-scoped', reason: null },
|
|
}),
|
|
},
|
|
connection: null,
|
|
backend: null,
|
|
},
|
|
],
|
|
});
|
|
storeState.skillsUserCatalog = [makeUserSkill(), makeCodexSkill()];
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const codexOnlyButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
|
button.textContent?.includes('Codex only')
|
|
);
|
|
expect(codexOnlyButton).toBeDefined();
|
|
|
|
await act(async () => {
|
|
(codexOnlyButton as HTMLButtonElement).click();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Codex Helper');
|
|
expect(host.textContent).not.toContain('Review Helper');
|
|
|
|
storeState.cliStatus = {
|
|
...storeState.cliStatus,
|
|
providers: storeState.cliStatus.providers.filter((provider) => provider.providerId !== 'codex'),
|
|
};
|
|
storeState.skillsUserCatalog = [makeUserSkill()];
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
React.createElement(SkillsPanel, {
|
|
projectPath: '/tmp/project-a',
|
|
projectLabel: 'Project A',
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
})
|
|
);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Review Helper');
|
|
expect(host.textContent).not.toContain('Codex Helper');
|
|
expect(host.textContent).not.toContain('No skills yet');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
});
|