mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test(e2e): add stable UI hooks (#2421)
This commit is contained in:
@@ -265,6 +265,7 @@ const SettingsHome = () => {
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
testId={`settings-nav-${item.id}`}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === flatItems.length - 1}
|
||||
|
||||
@@ -45,6 +45,7 @@ const SettingsSectionPage = ({ title, description, items }: SettingsSectionPageP
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={() => navigateToSettings(item.route)}
|
||||
testId={`settings-nav-${item.id}`}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
|
||||
@@ -107,6 +107,8 @@ describe('SettingsHome', () => {
|
||||
expect(screen.getByText('Advanced')).toBeInTheDocument();
|
||||
expect(screen.getByText('Clear App Data')).toBeInTheDocument();
|
||||
expect(screen.getByText('Log out')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-account')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-notifications')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('localizes Appearance and Mascot menu items', () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ interface SettingsMenuItemProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
onClick?: () => void;
|
||||
testId?: string;
|
||||
dangerous?: boolean;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
@@ -16,6 +17,7 @@ const SettingsMenuItem = ({
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
testId,
|
||||
dangerous = false,
|
||||
isFirst = false,
|
||||
isLast = false,
|
||||
@@ -49,6 +51,7 @@ const SettingsMenuItem = ({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-white dark:bg-neutral-900 text-stone-900 dark:text-neutral-100 ${borderClasses} hover:bg-stone-50 dark:hover:bg-neutral-800/60 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/60 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
|
||||
{content}
|
||||
@@ -58,6 +61,7 @@ const SettingsMenuItem = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={testId}
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-white dark:bg-neutral-900 text-stone-900 dark:text-neutral-100 ${borderClasses} ${roundedClasses}`}>
|
||||
{content}
|
||||
</div>
|
||||
|
||||
@@ -502,6 +502,7 @@ const DeveloperOptionsPanel = () => {
|
||||
title={t(item.titleKey)}
|
||||
description={t(item.descriptionKey)}
|
||||
onClick={() => navigateToSettings(item.route)}
|
||||
testId={`settings-nav-${item.id}`}
|
||||
isFirst={index === 0}
|
||||
isLast={false}
|
||||
/>
|
||||
@@ -513,6 +514,7 @@ const DeveloperOptionsPanel = () => {
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
testId={`settings-nav-${item.id}`}
|
||||
isFirst={false}
|
||||
isLast={index === trailingItems.length - 1}
|
||||
/>
|
||||
|
||||
@@ -172,7 +172,7 @@ const MemoryDebugPanel = () => {
|
||||
}, [clearNamespaceInput, refreshAll]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="memory-debug-panel">
|
||||
<SettingsHeader
|
||||
title={t('memory.debugTitle')}
|
||||
showBackButton={true}
|
||||
|
||||
@@ -155,7 +155,7 @@ const WebhooksDebugPanel = () => {
|
||||
}, [loadData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="webhooks-debug-panel">
|
||||
<SettingsHeader
|
||||
title={t('webhooks.debugTitle')}
|
||||
showBackButton={true}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (key: string) => key }) }));
|
||||
|
||||
vi.mock('../../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }),
|
||||
}));
|
||||
|
||||
vi.mock('../components/SettingsHeader', () => ({ default: () => null }));
|
||||
|
||||
vi.mock('../../../intelligence/MemoryTextWithEntities', () => ({
|
||||
MemoryTextWithEntities: ({ text }: { text: string }) => <span>{text}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('../../../../utils/tauriCommands', () => ({
|
||||
memoryClearNamespace: vi.fn().mockResolvedValue({}),
|
||||
memoryDeleteDocument: vi.fn().mockResolvedValue({}),
|
||||
memoryListDocuments: vi.fn().mockResolvedValue({ data: { documents: [] } }),
|
||||
memoryListNamespaces: vi.fn().mockResolvedValue([]),
|
||||
memoryQueryNamespace: vi.fn().mockResolvedValue({ matches: [] }),
|
||||
memoryRecallNamespace: vi.fn().mockResolvedValue({ matches: [] }),
|
||||
}));
|
||||
|
||||
describe('MemoryDebugPanel stable test hooks', () => {
|
||||
it('renders the panel-level test id', async () => {
|
||||
const { default: MemoryDebugPanel } = await import('../MemoryDebugPanel');
|
||||
|
||||
render(<MemoryDebugPanel />);
|
||||
|
||||
expect(screen.getByTestId('memory-debug-panel')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByText('Documents')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
* SSE-side observable behaviour (constructor URL, skip-on-null,
|
||||
* webhooks_debug event handling).
|
||||
*/
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Recording EventSource stub — jsdom has no native impl.
|
||||
@@ -112,6 +112,7 @@ describe('WebhooksDebugPanel — SSE auth wiring (#1922)', () => {
|
||||
|
||||
render(<WebhooksDebugPanel />);
|
||||
|
||||
expect(screen.getByTestId('webhooks-debug-panel')).toBeInTheDocument();
|
||||
await waitFor(() => expect(MockEventSource.instances).toHaveLength(1));
|
||||
expect(MockEventSource.instances[0].url).toBe(
|
||||
'http://localhost:7788/events/webhooks?token=rpc-token-debug-1'
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import UnifiedSkillCard from './SkillCard';
|
||||
|
||||
vi.mock('../../lib/i18n/I18nContext', () => ({
|
||||
useT: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
describe('UnifiedSkillCard stable test hooks', () => {
|
||||
it('renders row and primary action test ids', () => {
|
||||
const onCtaClick = vi.fn();
|
||||
|
||||
render(
|
||||
<UnifiedSkillCard
|
||||
icon={<span />}
|
||||
title="Calendar"
|
||||
description="Connect calendar context"
|
||||
ctaLabel="Install"
|
||||
testId="skill-row-calendar"
|
||||
ctaTestId="skill-install-calendar"
|
||||
onCtaClick={onCtaClick}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('skill-row-calendar')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('skill-install-calendar'));
|
||||
expect(onCtaClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders secondary action test ids', () => {
|
||||
const onUninstall = vi.fn();
|
||||
|
||||
render(
|
||||
<UnifiedSkillCard
|
||||
icon={<span />}
|
||||
title="Calendar"
|
||||
description="Connect calendar context"
|
||||
ctaLabel="Open"
|
||||
onCtaClick={vi.fn()}
|
||||
secondaryActions={[
|
||||
{
|
||||
label: 'Uninstall',
|
||||
icon: <span />,
|
||||
testId: 'skill-uninstall-calendar',
|
||||
onClick: onUninstall,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTitle('skills.card.moreActions'));
|
||||
fireEvent.click(screen.getByTestId('skill-uninstall-calendar'));
|
||||
expect(onUninstall).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,8 @@ export interface UnifiedSkillCardProps {
|
||||
ctaLabel: string;
|
||||
ctaVariant?: 'primary' | 'sage' | 'amber';
|
||||
onCtaClick: () => void;
|
||||
testId?: string;
|
||||
ctaTestId?: string;
|
||||
badge?: ReactNode;
|
||||
secondaryActions?: Array<{
|
||||
label: string;
|
||||
@@ -46,6 +48,8 @@ export function UnifiedSkillCard({
|
||||
ctaLabel,
|
||||
ctaVariant = 'primary',
|
||||
onCtaClick,
|
||||
testId,
|
||||
ctaTestId,
|
||||
secondaryActions,
|
||||
syncProgress,
|
||||
syncSummaryText,
|
||||
@@ -69,7 +73,9 @@ export function UnifiedSkillCard({
|
||||
const ctaStyle = CTA_STYLES[ctaVariant] ?? CTA_STYLES.primary;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-xl border border-stone-100 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3 transition-colors hover:bg-stone-50 dark:hover:bg-neutral-800/60">
|
||||
<div
|
||||
data-testid={testId}
|
||||
className="flex items-center gap-3 rounded-xl border border-stone-100 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3 transition-colors hover:bg-stone-50 dark:hover:bg-neutral-800/60">
|
||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center text-stone-600 dark:text-neutral-300">
|
||||
{icon}
|
||||
</div>
|
||||
@@ -155,6 +161,7 @@ export function UnifiedSkillCard({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
data-testid={ctaTestId}
|
||||
disabled={ctaDisabled}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1244,6 +1244,7 @@ const Conversations = ({ variant = 'page', composer = 'text' }: ConversationsPro
|
||||
</h2>
|
||||
{/* [#1123] welcomeLocked guard removed — always show new thread button */}
|
||||
<button
|
||||
data-testid="new-thread-sidebar-button"
|
||||
onClick={() => void handleCreateNewThread()}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60 text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 transition-colors"
|
||||
title={t('chat.newThread')}>
|
||||
@@ -1279,6 +1280,7 @@ const Conversations = ({ variant = 'page', composer = 'text' }: ConversationsPro
|
||||
sortedThreads.map(thread => (
|
||||
<div
|
||||
key={thread.id}
|
||||
data-testid={`thread-row-${thread.id}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
@@ -1433,6 +1435,7 @@ const Conversations = ({ variant = 'page', composer = 'text' }: ConversationsPro
|
||||
</div>
|
||||
<TokenUsagePill />
|
||||
<button
|
||||
data-testid="new-thread-button"
|
||||
onClick={() => void handleCreateNewThread()}
|
||||
className="px-2.5 py-1 rounded-lg text-xs font-medium text-primary-600 hover:bg-primary-50 transition-colors"
|
||||
title={t('chat.newThreadShortcut')}>
|
||||
@@ -2027,6 +2030,7 @@ const Conversations = ({ variant = 'page', composer = 'text' }: ConversationsPro
|
||||
{/* Voice input mic hidden per #717 (inputMode='voice' path retained). */}
|
||||
</div>
|
||||
<button
|
||||
data-testid="send-message-button"
|
||||
aria-label={t('chat.send')}
|
||||
title={t('chat.send')}
|
||||
onClick={() => {
|
||||
|
||||
+36
-17
@@ -125,6 +125,7 @@ interface ComposioConnectorTileProps {
|
||||
meta: ComposioToolkitMeta;
|
||||
connection: ComposioConnection | undefined;
|
||||
hasComposioError: boolean;
|
||||
testId?: string;
|
||||
onOpen: () => void;
|
||||
onRetryGlobal: () => void;
|
||||
}
|
||||
@@ -133,6 +134,7 @@ function ComposioConnectorTile({
|
||||
meta,
|
||||
connection,
|
||||
hasComposioError,
|
||||
testId,
|
||||
onOpen,
|
||||
onRetryGlobal,
|
||||
}: ComposioConnectorTileProps) {
|
||||
@@ -169,6 +171,7 @@ function ComposioConnectorTile({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={testId}
|
||||
onClick={handleClick}
|
||||
title={`${meta.name} — ${meta.description}`}
|
||||
aria-label={`${meta.name}, ${statusLabel}. ${ctaLabel}.`}
|
||||
@@ -205,10 +208,11 @@ interface ChannelTileProps {
|
||||
def: ChannelDefinition;
|
||||
status: ChannelConnectionStatus;
|
||||
icon: React.ReactNode;
|
||||
testId?: string;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
function ChannelTile({ def, status, icon, onOpen }: ChannelTileProps) {
|
||||
function ChannelTile({ def, status, icon, testId, onOpen }: ChannelTileProps) {
|
||||
const { t } = useT();
|
||||
const isConnected = status === 'connected';
|
||||
const isPending = status === 'connecting';
|
||||
@@ -219,6 +223,7 @@ function ChannelTile({ def, status, icon, onOpen }: ChannelTileProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={testId}
|
||||
onClick={onOpen}
|
||||
title={`${def.display_name} — ${def.description}`}
|
||||
aria-label={`${def.display_name}, ${statusLabel}. ${ctaLabel}.`}
|
||||
@@ -605,6 +610,8 @@ export default function Skills() {
|
||||
statusColor={screenIntelligenceStatus.statusColor}
|
||||
ctaLabel={screenIntelligenceStatus.ctaLabel}
|
||||
ctaVariant={screenIntelligenceStatus.ctaVariant}
|
||||
testId={`skill-row-${item.id}`}
|
||||
ctaTestId={`skill-install-${item.id}`}
|
||||
onCtaClick={() => {
|
||||
if (screenIntelligenceStatus.platformUnsupported) {
|
||||
navigate(item.route!);
|
||||
@@ -633,6 +640,8 @@ export default function Skills() {
|
||||
statusColor={autocompleteStatus.statusColor}
|
||||
ctaLabel={autocompleteStatus.ctaLabel}
|
||||
ctaVariant={autocompleteStatus.ctaVariant}
|
||||
testId={`skill-row-${item.id}`}
|
||||
ctaTestId={`skill-install-${item.id}`}
|
||||
onCtaClick={() => {
|
||||
if (
|
||||
autocompleteStatus.platformUnsupported ||
|
||||
@@ -658,6 +667,8 @@ export default function Skills() {
|
||||
statusColor={voiceStatus.statusColor}
|
||||
ctaLabel={voiceStatus.ctaLabel}
|
||||
ctaVariant={voiceStatus.ctaVariant}
|
||||
testId={`skill-row-${item.id}`}
|
||||
ctaTestId={`skill-install-${item.id}`}
|
||||
onCtaClick={() => {
|
||||
if (
|
||||
voiceStatus.connectionStatus === 'connected' ||
|
||||
@@ -679,6 +690,8 @@ export default function Skills() {
|
||||
title={item.name}
|
||||
description={item.description}
|
||||
ctaLabel={t('nav.settings')}
|
||||
testId={`skill-row-${item.id}`}
|
||||
ctaTestId={`skill-install-${item.id}`}
|
||||
onCtaClick={() => navigate(item.route!)}
|
||||
/>
|
||||
);
|
||||
@@ -710,6 +723,8 @@ export default function Skills() {
|
||||
statusLabel={scopeLabel}
|
||||
statusColor={scopeColor}
|
||||
ctaLabel={t('common.seeAll')}
|
||||
testId={`skill-row-${skill.id}`}
|
||||
ctaTestId={`skill-install-${skill.id}`}
|
||||
onCtaClick={() => {
|
||||
console.debug('[skills][discovered] open drawer', { skillId: skill.id });
|
||||
setSelectedSkill(skill);
|
||||
@@ -719,7 +734,7 @@ export default function Skills() {
|
||||
? [
|
||||
{
|
||||
label: t('skills.disconnect'),
|
||||
testId: `uninstall-skill-${skill.id}`,
|
||||
testId: `skill-uninstall-${skill.id}`,
|
||||
icon: (
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
@@ -826,13 +841,15 @@ export default function Skills() {
|
||||
className="grid gap-2 sm:gap-3"
|
||||
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(5.5rem, 1fr))' }}>
|
||||
{channelsGroup.items.map(item => (
|
||||
<ChannelTile
|
||||
key={item.id}
|
||||
def={item.channelDef!}
|
||||
status={item.channelStatus!}
|
||||
icon={item.icon}
|
||||
onOpen={() => setChannelModalDef(item.channelDef!)}
|
||||
/>
|
||||
<div key={item.id} data-testid={`skill-row-${item.id}`}>
|
||||
<ChannelTile
|
||||
def={item.channelDef!}
|
||||
status={item.channelStatus!}
|
||||
icon={item.icon}
|
||||
testId={`skill-install-${item.id}`}
|
||||
onOpen={() => setChannelModalDef(item.channelDef!)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -864,14 +881,16 @@ export default function Skills() {
|
||||
className="grid gap-2 sm:gap-3"
|
||||
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(5.5rem, 1fr))' }}>
|
||||
{composioSortedEntries.map(({ meta, connection }) => (
|
||||
<ComposioConnectorTile
|
||||
key={meta.slug}
|
||||
meta={meta}
|
||||
connection={connection}
|
||||
hasComposioError={Boolean(composioError)}
|
||||
onOpen={() => setComposioModalToolkit(meta)}
|
||||
onRetryGlobal={() => void refreshComposio()}
|
||||
/>
|
||||
<div key={meta.slug} data-testid={`skill-row-composio-${meta.slug}`}>
|
||||
<ComposioConnectorTile
|
||||
meta={meta}
|
||||
connection={connection}
|
||||
hasComposioError={Boolean(composioError)}
|
||||
testId={`skill-install-composio-${meta.slug}`}
|
||||
onOpen={() => setComposioModalToolkit(meta)}
|
||||
onRetryGlobal={() => void refreshComposio()}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -74,10 +74,10 @@ describe('Skills page — discovered skill cards', () => {
|
||||
expect(within(legacyRow as HTMLElement).getByText('Legacy').className).toMatch(/stone-600/);
|
||||
|
||||
// Uninstall surfaces for user-scope, non-legacy only.
|
||||
expect(screen.queryByTestId('uninstall-skill-user-skill')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('skill-uninstall-user-skill')).not.toBeInTheDocument();
|
||||
const userMore = within(userRow as HTMLElement).getByTitle('More actions');
|
||||
fireEvent.click(userMore);
|
||||
expect(await screen.findByTestId('uninstall-skill-user-skill')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('skill-uninstall-user-skill')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the detail drawer when the View CTA is clicked', async () => {
|
||||
|
||||
Reference in New Issue
Block a user