From 917f3f11712664d18900764131e7734d2ced9072 Mon Sep 17 00:00:00 2001 From: "cyrus@tinyhumans.ai" Date: Thu, 25 Jun 2026 15:32:26 +0530 Subject: [PATCH] fix(channels): unify the messaging default-channel selector into one grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Messaging settings panel rendered the channel list twice — a grid of connect/configure tiles plus a separate "Default Messaging Channel" button list, both labeled the same — so it was unclear which one set the default. Collapse them into a single horizontal-card grid where each tile shows connection status, opens setup on click, and owns the default selection: - icon on the left; name -> status -> default control stacked on the right - "Set as default" appears only on connected channels; the current default shows a Default badge plus a primary ring on the tile - connected and not-connected channels render as two separate rows - Web stays selectable (always available) Add channels.setAsDefault / channels.defaultBadge i18n keys across all locales. Update channel-grid unit tests and the e2e/playwright specs that switched the default (now via the always-connected Web tile). --- app/src/lib/i18n/ar.ts | 2 + app/src/lib/i18n/bn.ts | 2 + app/src/lib/i18n/de.ts | 2 + app/src/lib/i18n/en.ts | 2 + app/src/lib/i18n/es.ts | 2 + app/src/lib/i18n/fr.ts | 2 + app/src/lib/i18n/hi.ts | 2 + app/src/lib/i18n/id.ts | 2 + app/src/lib/i18n/it.ts | 2 + app/src/lib/i18n/ko.ts | 2 + app/src/lib/i18n/pl.ts | 2 + app/src/lib/i18n/pt.ts | 2 + app/src/lib/i18n/ru.ts | 2 + app/src/lib/i18n/zh-CN.ts | 2 + app/src/pages/Skills.tsx | 203 ++++++++++++------ .../__tests__/Skills.channels-grid.test.tsx | 98 ++++++++- .../settings-channels-permissions.spec.ts | 65 +++--- .../settings-feature-preferences.spec.ts | 24 ++- .../settings-channels-permissions.spec.ts | 19 +- .../settings-feature-preferences.spec.ts | 19 +- 20 files changed, 347 insertions(+), 109 deletions(-) diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 9e1a16f670..57051d6cfa 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -902,6 +902,8 @@ const messages: TranslationMap = { 'channels.status.error': 'خطأ', 'channels.status.configuring': 'جارٍ الضبط', 'channels.defaultMessaging': 'قناة المراسلة الافتراضية', + 'channels.setAsDefault': 'تعيين كافتراضي', + 'channels.defaultBadge': 'افتراضي', 'webhooks.title': 'الـ Webhooks', 'webhooks.create': 'إنشاء Webhook', 'webhooks.noWebhooks': 'لا توجد Webhooks مضبوطة', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 1bee18079b..92adf39614 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -925,6 +925,8 @@ const messages: TranslationMap = { 'channels.status.error': 'ত্রুটি', 'channels.status.configuring': 'কনফিগার হচ্ছে', 'channels.defaultMessaging': 'ডিফল্ট মেসেজিং চ্যানেল', + 'channels.setAsDefault': 'ডিফল্ট হিসেবে সেট করুন', + 'channels.defaultBadge': 'ডিফল্ট', 'webhooks.title': 'ওয়েবহুক', 'webhooks.create': 'Webhook তৈরি করুন', 'webhooks.noWebhooks': 'কোনো webhook কনফিগার করা হয়নি', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 886cae97a3..10dc8844f7 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -948,6 +948,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Fehler', 'channels.status.configuring': 'Konfigurieren', 'channels.defaultMessaging': 'Standard-Messaging-Kanal', + 'channels.setAsDefault': 'Als Standard festlegen', + 'channels.defaultBadge': 'Standard', 'webhooks.title': 'Webhooks', 'webhooks.create': 'Erstelle einen Webhook', 'webhooks.noWebhooks': 'Keine Webhooks konfiguriert', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index dfc84facc7..5dc1290946 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -1258,6 +1258,8 @@ const en: TranslationMap = { 'channels.status.error': 'Error', 'channels.status.configuring': 'Configuring', 'channels.defaultMessaging': 'Default Messaging Channel', + 'channels.setAsDefault': 'Set as default', + 'channels.defaultBadge': 'Default', // Webhooks 'webhooks.title': 'Webhooks', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index a5eb55d0f6..9b0ce59e65 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -943,6 +943,8 @@ const messages: TranslationMap = { 'channels.status.error': 'error', 'channels.status.configuring': 'Configurando', 'channels.defaultMessaging': 'Canal de mensajería predeterminado', + 'channels.setAsDefault': 'Establecer como predeterminado', + 'channels.defaultBadge': 'Predeterminado', 'webhooks.title': 'Ganchos web', 'webhooks.create': 'Crear webhook', 'webhooks.noWebhooks': 'Sin webhooks configurados', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 2be47ecaa6..c58da8f754 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -946,6 +946,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Erreur', 'channels.status.configuring': 'Configuration en cours', 'channels.defaultMessaging': 'Canal de messagerie par défaut', + 'channels.setAsDefault': 'Définir par défaut', + 'channels.defaultBadge': 'Par défaut', 'webhooks.title': 'Webhooks', 'webhooks.create': 'Créer un webhook', 'webhooks.noWebhooks': 'Aucun webhook configuré', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 1215abcf53..0dbd720fe7 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -921,6 +921,8 @@ const messages: TranslationMap = { 'channels.status.error': 'एरर', 'channels.status.configuring': 'कॉन्फिगर हो रहा है', 'channels.defaultMessaging': 'डिफ़ॉल्ट मैसेजिंग चैनल', + 'channels.setAsDefault': 'डिफ़ॉल्ट के रूप में सेट करें', + 'channels.defaultBadge': 'डिफ़ॉल्ट', 'webhooks.title': 'वेबहुक्स', 'webhooks.create': 'Webhook बनाएं', 'webhooks.noWebhooks': 'कोई webhook कॉन्फिगर नहीं है', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index fc59d79241..62f4a37fcf 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -928,6 +928,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Kesalahan', 'channels.status.configuring': 'Mengonfigurasi', 'channels.defaultMessaging': 'Kanal Pesan Default', + 'channels.setAsDefault': 'Jadikan bawaan', + 'channels.defaultBadge': 'Bawaan', 'webhooks.title': 'Webhook', 'webhooks.create': 'Buat Webhook', 'webhooks.noWebhooks': 'Belum ada webhook yang dikonfigurasi', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index a161dab496..3916f6697a 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -940,6 +940,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Errore', 'channels.status.configuring': 'Configurazione', 'channels.defaultMessaging': 'Canale di messaggistica predefinito', + 'channels.setAsDefault': 'Imposta come predefinito', + 'channels.defaultBadge': 'Predefinito', 'webhooks.title': 'Webhook', 'webhooks.create': 'Crea webhook', 'webhooks.noWebhooks': 'Nessun webhook configurato', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 83028b1ef5..dbdae564c8 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -921,6 +921,8 @@ const messages: TranslationMap = { 'channels.status.error': '오류', 'channels.status.configuring': '구성 중', 'channels.defaultMessaging': '기본 메시징 채널', + 'channels.setAsDefault': '기본값으로 설정', + 'channels.defaultBadge': '기본값', 'webhooks.title': '웹훅', 'webhooks.create': '웹훅 생성', 'webhooks.noWebhooks': '구성된 웹훅이 없습니다', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index ac4ff09f55..30c35e16d4 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -935,6 +935,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Błąd', 'channels.status.configuring': 'Konfigurowanie', 'channels.defaultMessaging': 'Domyślny kanał wiadomości', + 'channels.setAsDefault': 'Ustaw jako domyślny', + 'channels.defaultBadge': 'Domyślny', 'webhooks.title': 'Webhooki', 'webhooks.create': 'Utwórz webhook', 'webhooks.noWebhooks': 'Brak skonfigurowanych webhooków', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index a04a15c116..93f0ad77ea 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -945,6 +945,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Erro', 'channels.status.configuring': 'Configurando', 'channels.defaultMessaging': 'Canal de Mensagens Padrão', + 'channels.setAsDefault': 'Definir como padrão', + 'channels.defaultBadge': 'Padrão', 'webhooks.title': 'Webhooks', 'webhooks.create': 'Criar Webhook', 'webhooks.noWebhooks': 'Nenhum webhook configurado', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 7fb7b797af..5facab53e7 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -932,6 +932,8 @@ const messages: TranslationMap = { 'channels.status.error': 'Ошибка', 'channels.status.configuring': 'Настройка', 'channels.defaultMessaging': 'Канал по умолчанию', + 'channels.setAsDefault': 'Сделать по умолчанию', + 'channels.defaultBadge': 'По умолчанию', 'webhooks.title': 'Вебхуки', 'webhooks.create': 'Создать вебхук', 'webhooks.noWebhooks': 'Вебхуки не настроены', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 91140ba13b..65c72f0285 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -881,6 +881,8 @@ const messages: TranslationMap = { 'channels.status.error': '错误', 'channels.status.configuring': '配置中', 'channels.defaultMessaging': '默认消息渠道', + 'channels.setAsDefault': '设为默认', + 'channels.defaultBadge': '默认', 'webhooks.title': 'Webhook', 'webhooks.create': '创建 Webhook', 'webhooks.noWebhooks': '未配置任何 Webhook', diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx index ebc5358775..e057cc8e3b 100644 --- a/app/src/pages/Skills.tsx +++ b/app/src/pages/Skills.tsx @@ -256,9 +256,27 @@ interface ChannelTileProps { icon: React.ReactNode; testId?: string; onOpen: () => void; + /** Whether this channel is the current default messaging channel. */ + isDefault: boolean; + /** Set this channel as the default. */ + onSetDefault: () => void; + /** Test id for the "set as default" control (kept stable for E2E). */ + setDefaultTestId?: string; + /** Disable the default control while a write is in flight. */ + setDefaultBusy?: boolean; } -function ChannelTile({ def, status, icon, testId, onOpen }: ChannelTileProps) { +function ChannelTile({ + def, + status, + icon, + testId, + onOpen, + isDefault, + onSetDefault, + setDefaultTestId, + setDefaultBusy, +}: ChannelTileProps) { const { t } = useT(); const isConnected = status === 'connected'; const isPending = status === 'connecting'; @@ -266,34 +284,81 @@ function ChannelTile({ def, status, icon, testId, onOpen }: ChannelTileProps) { const statusLabel = channelStatusLabel(status, t); const ctaLabel = isConnected ? t('skills.configure') : t('channels.setup'); + // Horizontal tile: icon on the left; name → status → default control stacked + // on the right. The tile is a container (not one button) so "configure" (the + // icon + name row) and "set as default" stay distinct, focusable controls — + // collapsing the old two-selector layout (connect grid + separate picker) + // into one place. "Set as default" only appears for channels you can actually + // route through (connected); the default badge still shows on whichever + // channel is persisted as default, connected or not. + const showDefaultControl = isDefault || isConnected; + return ( - + + {showDefaultControl && ( + // Aligns under the name/status text (icon 2.5rem + gap 0.75rem). +
+ {isDefault ? ( + + + {t('channels.defaultBadge')} + + ) : ( + + )} +
+ )} + ); } @@ -1031,49 +1096,55 @@ export default function Skills() { {t('channels.defaultMessaging')}

-
- {channelsGroup.items.map(item => ( -
- setChannelModalDef(item.channelDef!)} - /> + {/* One unified surface: each tile shows connection status, + opens setup/configure on click, and owns the "default + messaging channel" selection via its footer control. + Connected channels and not-yet-connected channels are + rendered as two separate grids (no divider/label) so + each group occupies its own rows. */} + {(() => { + // The built-in web channel needs no connection — treat it + // as always available so it stays selectable as default. + const statusFor = (def: ChannelDefinition): ChannelConnectionStatus => + def.id === 'web' ? 'connected' : bestChannelStatus(def.id as ChannelType); + const renderTile = (def: ChannelDefinition) => { + const channelId = def.id as ChannelType; + return ( +
+ setChannelModalDef(def)} + isDefault={channelConnections.defaultMessagingChannel === channelId} + onSetDefault={() => void handleSetDefaultChannel(channelId)} + setDefaultTestId={`channel-select-${channelId}`} + setDefaultBusy={defaultChannelBusy !== null} + /> +
+ ); + }; + const connected = channelDefs.filter(d => statusFor(d) === 'connected'); + const notConnected = channelDefs.filter(d => statusFor(d) !== 'connected'); + const gridStyle = { + gridTemplateColumns: 'repeat(auto-fill, minmax(13rem, 1fr))', + }; + return ( +
+ {connected.length > 0 && ( +
+ {connected.map(renderTile)} +
+ )} + {notConnected.length > 0 && ( +
+ {notConnected.map(renderTile)} +
+ )}
- ))} -
- -
-
- {t('channels.defaultMessaging')} -
-
- {channelDefs.map(def => { - const channelId = def.id as ChannelType; - const selected = - channelConnections.defaultMessagingChannel === channelId; - return ( - - ); - })} -
-
+ ); + })()}
)} diff --git a/app/src/pages/__tests__/Skills.channels-grid.test.tsx b/app/src/pages/__tests__/Skills.channels-grid.test.tsx index 78df779d1c..14a4c5d728 100644 --- a/app/src/pages/__tests__/Skills.channels-grid.test.tsx +++ b/app/src/pages/__tests__/Skills.channels-grid.test.tsx @@ -24,14 +24,33 @@ const imessageDef: ChannelDefinition = { capabilities: [], }; +// The built-in web channel needs no connection, so the grid always renders it +// as connected — making it the reliable "connected, not default" tile. +const webDef: ChannelDefinition = { + id: 'web', + display_name: 'Web', + description: 'Chat via the built-in web UI.', + icon: 'web', + auth_modes: [], + capabilities: [], +}; + vi.mock('../../hooks/useChannelDefinitions', () => ({ useChannelDefinitions: () => ({ - definitions: [telegramDef, imessageDef], + definitions: [telegramDef, imessageDef, webDef], loading: false, error: null, }), })); +const { updatePreferencesMock } = vi.hoisted(() => ({ + updatePreferencesMock: vi.fn<(channel: string) => Promise>(), +})); + +vi.mock('../../services/api/channelConnectionsApi', () => ({ + channelConnectionsApi: { updatePreferences: (channel: string) => updatePreferencesMock(channel) }, +})); + vi.mock('../../services/api/workflowsApi', async () => { const actual = await vi.importActual( '../../services/api/workflowsApi' @@ -64,6 +83,76 @@ vi.mock('../../lib/composio/hooks', () => ({ describe('Skills page — Channels grid', () => { beforeEach(() => { // The default tab is 'composio'; click 'Messaging' to reveal the Channels card. + updatePreferencesMock.mockReset(); + updatePreferencesMock.mockResolvedValue(undefined); + }); + + it('groups connected channels ahead of not-connected ones', () => { + const preloadedState = { + channelConnections: { + schemaVersion: 1, + migrationCompleted: true, + defaultMessagingChannel: 'web' as const, + connections: { + telegram: { + managed_dm: undefined, + oauth: { + channel: 'telegram' as const, + authMode: 'oauth' as const, + status: 'connected' as const, + selectedDefault: false, + lastError: null, + capabilities: [], + updatedAt: new Date().toISOString(), + }, + bot_token: undefined, + api_key: undefined, + }, + }, + }, + }; + + renderWithProviders(, { initialEntries: ['/connections'], preloadedState }); + fireEvent.click(screen.getByTestId('two-pane-nav-channels')); + + const channelsCard = screen.getByRole('heading', { name: 'Messaging' }).closest('.rounded-2xl'); + const order = within(channelsCard as HTMLElement) + .getAllByTestId(/^skill-row-channel-/) + .map(el => el.getAttribute('data-testid')); + + // Connected channels (Telegram + always-on Web) come before the + // disconnected one (iMessage), with no separator between the groups. + const idxTelegram = order.indexOf('skill-row-channel-telegram'); + const idxWeb = order.indexOf('skill-row-channel-web'); + const idxImessage = order.indexOf('skill-row-channel-imessage'); + expect(idxTelegram).toBeGreaterThanOrEqual(0); + expect(idxWeb).toBeGreaterThanOrEqual(0); + expect(idxImessage).toBeGreaterThan(idxTelegram); + expect(idxImessage).toBeGreaterThan(idxWeb); + }); + + it('offers "set as default" only on connected tiles and switches the default', async () => { + const { store } = renderWithProviders(, { initialEntries: ['/connections'] }); + fireEvent.click(screen.getByTestId('two-pane-nav-channels')); + + const channelsCard = screen.getByRole('heading', { name: 'Messaging' }).closest('.rounded-2xl'); + const within$ = within(channelsCard as HTMLElement); + + // The redux default starts on Telegram, so its tile shows the "Default" + // badge (there is no longer a second, separate channel-picker list below). + expect(within$.getByTestId('channel-select-telegram')).toHaveTextContent('Default'); + // Web is always connected but not the default → offers "Set as default". + const setWebDefault = within$.getByTestId('channel-select-web'); + expect(setWebDefault).toHaveTextContent('Set as default'); + // A disconnected, non-default channel offers no default control at all. + expect(within$.queryByTestId('channel-select-imessage')).toBeNull(); + + fireEvent.click(setWebDefault); + + await vi.waitFor(() => + expect(store.getState().channelConnections.defaultMessagingChannel).toBe('web') + ); + expect(updatePreferencesMock).toHaveBeenCalledWith('web'); }); it('renders configured channels as tiles in a dedicated card and opens the setup modal on click', async () => { @@ -143,7 +232,12 @@ describe('Skills page — Channels grid', () => { const telegramTile = within(channelsCard as HTMLElement).getByRole('button', { name: new RegExp(`Telegram.*${labelPattern.source}`, 'i'), }); - expect(telegramTile.className).toMatch(classPattern); + // The connection-status colour now lives on the tile container (the + // inner button only owns the "configure" affordance), so assert against + // the wrapping tile rather than the button itself. + const tileContainer = telegramTile.closest('.rounded-2xl'); + expect(tileContainer).not.toBeNull(); + expect((tileContainer as HTMLElement).className).toMatch(classPattern); } ); diff --git a/app/test/e2e/specs/settings-channels-permissions.spec.ts b/app/test/e2e/specs/settings-channels-permissions.spec.ts index 155e64490f..faba179be8 100644 --- a/app/test/e2e/specs/settings-channels-permissions.spec.ts +++ b/app/test/e2e/specs/settings-channels-permissions.spec.ts @@ -11,11 +11,26 @@ * - 13.2.2 Privacy panel renders + analytics toggle is present */ import { waitForApp } from '../helpers/app-helpers'; +import { callOpenhumanRpc } from '../helpers/core-rpc'; import { clickSelector, textExists, waitForText } from '../helpers/element-helpers'; import { resetApp } from '../helpers/reset-app'; import { navigateViaHash } from '../helpers/shared-flows'; import { startMockServer, stopMockServer } from '../mock-server'; +/** Read the persisted default messaging channel from the renderer's redux store. */ +async function defaultMessagingChannel(): Promise { + return browser.execute(() => { + const win = window as unknown as { + __OPENHUMAN_STORE__?: { + getState?: () => { channelConnections?: { defaultMessagingChannel?: string | null } }; + }; + }; + return ( + win.__OPENHUMAN_STORE__?.getState?.().channelConnections?.defaultMessagingChannel ?? null + ); + }); +} + const USER_ID = 'e2e-settings-channels'; describe('Settings - Channels & Permissions', () => { @@ -30,35 +45,37 @@ describe('Settings - Channels & Permissions', () => { }); it('allows switching default messaging channel (13.2.1)', async () => { - // Phase 2: Default Messaging Channel UI is at /connections (Messaging tab). - // Old /skills?tab=channels → /connections?tab=messaging. + // The messaging panel now offers "Set as default" only on *connected* + // channels. In a fresh workspace the only always-connected channel is Web + // (the built-in chat), so we make Telegram the default first — that turns + // Web into a connected, non-default tile that exposes the control — then + // switch the default to Web through the UI. + await callOpenhumanRpc('openhuman.channels_set_default', { channel: 'telegram' }); + + // Navigate away and back so the messaging panel re-seeds the default from + // the core (it reads the persisted default when the page mounts). + await navigateViaHash('/home'); await navigateViaHash('/connections?tab=messaging'); await waitForText('Default Messaging Channel', 15_000); expect(await textExists('Telegram')).toBe(true); - expect(await textExists('Discord')).toBe(true); + expect(await textExists('Web')).toBe(true); - // Select via the stable channel-select test id rather than the ambiguous - // "Discord" text (which also appears on connection tiles / help copy). - await clickSelector('[data-testid="channel-select-discord"]'); - // Confirm the selection persisted to redux state (the Connections messaging - // tab no longer renders the legacy "Active route" line). - await browser.waitUntil( - async () => - (await browser.execute(() => { - const win = window as unknown as { - __OPENHUMAN_STORE__?: { - getState?: () => { channelConnections?: { defaultMessagingChannel?: string | null } }; - }; - }; - return win.__OPENHUMAN_STORE__?.getState?.().channelConnections?.defaultMessagingChannel; - })) === 'discord', - { - timeout: 10_000, - interval: 500, - timeoutMsg: 'default messaging channel did not switch to discord', - } - ); + // Confirm the panel seeded Telegram as the default before we switch. + await browser.waitUntil(async () => (await defaultMessagingChannel()) === 'telegram', { + timeout: 10_000, + interval: 500, + timeoutMsg: 'messaging panel did not seed Telegram as the default', + }); + + // Switch to Web via the stable channel-select test id (its "Set as default" + // control is present because Web is always connected). + await clickSelector('[data-testid="channel-select-web"]'); + await browser.waitUntil(async () => (await defaultMessagingChannel()) === 'web', { + timeout: 10_000, + interval: 500, + timeoutMsg: 'default messaging channel did not switch to web', + }); }); it('renders privacy settings and analytics toggle (13.2.2)', async () => { diff --git a/app/test/e2e/specs/settings-feature-preferences.spec.ts b/app/test/e2e/specs/settings-feature-preferences.spec.ts index 284a2300a7..ec0617417f 100644 --- a/app/test/e2e/specs/settings-feature-preferences.spec.ts +++ b/app/test/e2e/specs/settings-feature-preferences.spec.ts @@ -82,15 +82,27 @@ describe('Settings - Feature Preferences', () => { }); it('persists the default messaging channel through redux state', async () => { - // Phase 2: Default Messaging Channel moved to /connections (Messaging tab). - // Old /skills?tab=channels → /connections?tab=messaging. + // The messaging panel exposes "Set as default" only on *connected* channels. + // In a fresh workspace the only always-connected channel is Web (built-in + // chat), so make Telegram the default first — that turns Web into a + // connected, non-default tile with the control — then switch to Web. + await callOpenhumanRpc('openhuman.channels_set_default', { channel: 'telegram' }); + + // Navigate away and back so the panel re-seeds the default from the core. + await navigateViaHash('/home'); await navigateViaHash('/connections?tab=messaging'); await waitForText('Default Messaging Channel', 15_000); - // Use the stable channel-select test id — "Discord" text also appears on - // connection tiles and help copy, so clickText could hit the wrong node. - await clickSelector('[data-testid="channel-select-discord"]', 10_000); - await browser.waitUntil(async () => (await defaultMessagingChannelFromStore()) === 'discord', { + await browser.waitUntil(async () => (await defaultMessagingChannelFromStore()) === 'telegram', { + timeout: 10_000, + interval: 500, + timeoutMsg: 'messaging panel did not seed Telegram as the default', + }); + + // Switch to Web via its stable channel-select test id (Web is always + // connected, so its "Set as default" control is present). + await clickSelector('[data-testid="channel-select-web"]', 10_000); + await browser.waitUntil(async () => (await defaultMessagingChannelFromStore()) === 'web', { timeout: 10_000, interval: 500, timeoutMsg: 'default channel did not update', diff --git a/app/test/playwright/specs/settings-channels-permissions.spec.ts b/app/test/playwright/specs/settings-channels-permissions.spec.ts index 815bf0128d..dd4d7bee05 100644 --- a/app/test/playwright/specs/settings-channels-permissions.spec.ts +++ b/app/test/playwright/specs/settings-channels-permissions.spec.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'; import { bootAuthenticatedPage, + callCoreRpc, dismissWalkthroughIfPresent, waitForAppReady, } from '../helpers/core-rpc'; @@ -27,7 +28,14 @@ test.describe('Settings - Channels & Permissions', () => { }); test('allows switching default messaging channel', async ({ page }) => { - // Phase 2: default messaging channel UI moved to /connections (Messaging tab) + // "Set as default" now appears only on *connected* channels. In a fresh + // workspace the only always-connected channel is Web (built-in chat), so + // make Telegram the default first (turning Web into a connected, + // non-default tile with the control), then switch the default to Web. + await callCoreRpc('openhuman.channels_set_default', { channel: 'telegram' }); + + // Phase 2: default messaging channel UI moved to /connections (Messaging tab). + // Mounting the panel re-seeds the default from the core. await page.goto('/#/connections?tab=messaging'); await waitForAppReady(page); await dismissWalkthroughIfPresent(page); @@ -39,10 +47,13 @@ test.describe('Settings - Channels & Permissions', () => { await expect(page.getByText('Default Messaging Channel').last()).toBeVisible(); await expect(page.getByText('Telegram').last()).toBeVisible(); - await expect(page.getByText('Discord').last()).toBeVisible(); + await expect(page.getByText('Web').last()).toBeVisible(); + + // Confirm the panel seeded Telegram as the default before switching. + await expect.poll(() => getDefaultMessagingChannel(page)).toBe('telegram'); - await page.getByText('Discord').last().click(); - await expect.poll(() => getDefaultMessagingChannel(page)).toBe('discord'); + await page.getByTestId('channel-select-web').click(); + await expect.poll(() => getDefaultMessagingChannel(page)).toBe('web'); }); test('renders privacy settings and analytics toggle', async ({ page }) => { diff --git a/app/test/playwright/specs/settings-feature-preferences.spec.ts b/app/test/playwright/specs/settings-feature-preferences.spec.ts index e73b505845..8ecc3e76c7 100644 --- a/app/test/playwright/specs/settings-feature-preferences.spec.ts +++ b/app/test/playwright/specs/settings-feature-preferences.spec.ts @@ -101,22 +101,27 @@ test.describe('Settings - Feature Preferences', () => { }); test('persists the default messaging channel through redux state', async ({ page }) => { - // Phase 2: default messaging channel moved to /connections (Messaging tab) + // Phase 2: default messaging channel moved to /connections (Messaging tab). await openAuthenticatedRoute(page, 'pw-settings-default-channel', '/connections?tab=messaging'); + // "Set as default" now appears only on *connected* channels. In a fresh + // workspace the only always-connected channel is Web (built-in chat), so + // make Telegram the default first (turning Web into a connected, + // non-default tile with the control), reload so the panel re-seeds, then + // switch the default to Web. + await callCoreRpc('openhuman.channels_set_default', { channel: 'telegram' }); + await reloadAndWait(page); + const messagingTab = page.getByTestId('two-pane-nav-channels'); if (await messagingTab.isVisible().catch(() => false)) { await messagingTab.click(); } await expect(page.getByText('Default Messaging Channel').last()).toBeVisible(); - await page - .locator('button') - .filter({ hasText: /^Discord$/ }) - .last() - .click(); + await expect.poll(() => getDefaultMessagingChannel(page)).toBe('telegram'); - await expect.poll(() => getDefaultMessagingChannel(page)).toBe('discord'); + await page.getByTestId('channel-select-web').click(); + await expect.poll(() => getDefaultMessagingChannel(page)).toBe('web'); }); test('persists tools preferences to the core app-state snapshot', async ({ page }) => {