diff --git a/CHANGELOG.md b/CHANGELOG.md index f762ef469a..e7e2cac202 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ ### Fixes * Fix: leaving a community now purges uploaded and downloaded files; if the leave is interrupted (process killed, OS-terminated, power loss), the purge is finished on the next app launch [#3225](https://github.com/TryQuiet/quiet/issues/3225) +* Fixed settings section titles rendering inside the scrollable content; moved section titles to the settings header and added a regression test to prevent future regressions [#2568](https://github.com/TryQuiet/quiet/issues/2568) * Fixed android not requesting permission for foreground push notifications [#3254](https://github.com/TryQuiet/quiet/issues/3254) * Fixes race condition with android push notifications [#3255](https://github.com/TryQuiet/quiet/issues/3255) * Mark IOS UI as needing compatibility updates to fix contrast problems on IOS 26 [#3266](https://github.com/TryQuiet/quiet/issues/3266) diff --git a/packages/desktop/src/renderer/components/Settings/SettingsComponent.test.tsx b/packages/desktop/src/renderer/components/Settings/SettingsComponent.test.tsx new file mode 100644 index 0000000000..0f30da8042 --- /dev/null +++ b/packages/desktop/src/renderer/components/Settings/SettingsComponent.test.tsx @@ -0,0 +1,245 @@ +import React from 'react' +import { render, screen, fireEvent, within, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom' +import { SettingsComponent } from './SettingsComponent' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const noOp = jest.fn() + +const mockLeaveCommunityModal = { + open: false, + handleOpen: jest.fn(), + handleClose: jest.fn(), +} + +const MockTab: React.FC<{ handleClose: () => void }> = ({ handleClose }) => ( +
+

Tab content

+ +
+) + +const defaultTabs: Record void }>> = { + about: MockTab, + notifications: MockTab, + attachments: MockTab, + invite: MockTab, + qrcode: MockTab, + leaveCommunity: MockTab, + debug: MockTab, +} + +function renderSettings(overrides: Partial> = {}) { + return render( + + ) +} + +// Click a menu item and wait for the tab drawer to fully mount. +// MUI Drawer only inserts children into the DOM when open=true, so we must +// wait after the state-updating click before querying the second drawer. +async function openTab(testId: string) { + fireEvent.click(screen.getByTestId(testId)) + return screen.findByTestId('ArrowBackIcon') // resolves once drawer is mounted +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('SettingsComponent', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + // ── Rendering ───────────────────────────────────────────────────────────── + + describe('main drawer', () => { + it('renders the "Community Settings" heading when open', () => { + renderSettings() + expect(screen.getByText('Community Settings')).toBeInTheDocument() + }) + + it('renders all expected menu items', () => { + renderSettings() + expect(screen.getByTestId('about-settings-tab')).toBeInTheDocument() + expect(screen.getByTestId('notifications-settings-tab')).toBeInTheDocument() + expect(screen.getByTestId('attachments-settings-tab')).toBeInTheDocument() + expect(screen.getByTestId('invite-settings-tab')).toBeInTheDocument() + expect(screen.getByTestId('qr-code-settings-tab')).toBeInTheDocument() + }) + + it('calls handleClose when the close button is clicked', () => { + const handleClose = jest.fn() + renderSettings({ handleClose }) + fireEvent.click(screen.getByTestId('CloseIcon')) + expect(handleClose).toHaveBeenCalledTimes(1) + }) + }) + + // ── Tab drawer – issue #2568 regression ─────────────────────────────────── + + describe('issue #2568 – tab title in top bar, not in content area', () => { + /** + * When a tab is opened the title must appear inside the header ListItem + * (the element that also contains the back-arrow button), NOT inside the + * scrollable content area below the divider. + * + * Both drawers stay in the DOM simultaneously once the tab is open, so + * some title strings (e.g. "Notifications") appear twice – once as a menu + * item in drawer 1 and once as the header title in drawer 2. We therefore + * scope every assertion to the specific container. + */ + it.each([ + ['about', 'About Quiet'], + ['notifications', 'Notifications'], + ['attachments', 'Files and Images'], + ['invite', 'Add Members'], + ['qr-code', 'QR Code'], + ])('"%s" tab shows "%s" in the header bar', async (testId, expectedTitle) => { + renderSettings() + + // openTab uses fireEvent + findByTestId (async) to wait for the second + // drawer to mount – fireEvent reliably triggers MUI's onClick handlers. + const backButton = await openTab(`${testId}-settings-tab`) + + // The title must be inside the same ListItem as the back button (= the header bar) + const headerListItem = backButton.closest('li') as HTMLElement + expect(headerListItem).not.toBeNull() + expect(within(headerListItem).getByText(expectedTitle)).toBeInTheDocument() + + // The scrollable content area must NOT contain the title + const tabContent = screen.getByTestId('mock-tab-content') + expect(within(tabContent).queryByText(expectedTitle)).not.toBeInTheDocument() + }) + }) + + // ── Tab navigation ──────────────────────────────────────────────────────── + + describe('tab navigation', () => { + it('opens the tab drawer when a menu item is clicked', async () => { + renderSettings() + expect(screen.queryByTestId('close-tab-button-box')).not.toBeInTheDocument() + + await openTab('notifications-settings-tab') + + expect(screen.getByTestId('ArrowBackIcon')).toBeInTheDocument() + }) + + it('renders the tab component content after opening a tab', async () => { + renderSettings() + await openTab('about-settings-tab') + + expect(screen.getByTestId('mock-tab-content')).toBeInTheDocument() + }) + + it('closes the tab drawer when the back button is clicked', async () => { + renderSettings() + const backButton = await openTab('notifications-settings-tab') + + fireEvent.click(backButton) + + await waitFor(() => { + expect(screen.queryByTestId('close-tab-button-box')).not.toBeInTheDocument() + }) + }) + + it('closes the tab drawer when the tab itself calls handleClose', async () => { + renderSettings() + await openTab('notifications-settings-tab') + + fireEvent.click(screen.getByText('Close from tab')) + + await waitFor(() => { + expect(screen.queryByTestId('mock-tab-content')).not.toBeInTheDocument() + }) + }) + + it('shows the correct title when switching between tabs', async () => { + renderSettings() + + // Open notifications and verify its title is in the header + const backButton1 = await openTab('notifications-settings-tab') + const header1 = backButton1.closest('li') as HTMLElement + expect(within(header1).getByText('Notifications')).toBeInTheDocument() + + // Go back to the main menu + fireEvent.click(backButton1) + await waitFor(() => { + expect(screen.queryByTestId('close-tab-button-box')).not.toBeInTheDocument() + }) + + // Open attachments and verify its title + const backButton2 = await openTab('attachments-settings-tab') + const header2 = backButton2.closest('li') as HTMLElement + expect(within(header2).getByText('Files and Images')).toBeInTheDocument() + }) + }) + + // ── isWindows prop ──────────────────────────────────────────────────────── + + describe('isWindows prop', () => { + it('hides the "Leave community" item on Windows', () => { + renderSettings({ isWindows: true }) + expect(screen.queryByTestId('leave-community-settings-tab')).not.toBeInTheDocument() + }) + + it('shows the "Leave community" item on non-Windows platforms', () => { + renderSettings({ isWindows: false }) + expect(screen.getByTestId('leave-community-settings-tab')).toBeInTheDocument() + }) + + it('shows the "Leave community" item when isWindows is undefined', () => { + renderSettings({ isWindows: undefined }) + expect(screen.getByTestId('leave-community-settings-tab')).toBeInTheDocument() + }) + }) + + // ── Debug tab (environment-gated) ───────────────────────────────────────── + + describe('debug tab', () => { + const originalEnv = process.env + + afterEach(() => { + process.env = { ...originalEnv } + }) + + it('shows the debug tab in development mode', () => { + process.env = { ...originalEnv, NODE_ENV: 'development' } + renderSettings() + expect(screen.getByTestId('debug-settings-tab')).toBeInTheDocument() + }) + + it('shows the debug tab when IS_E2E is "true"', () => { + process.env = { ...originalEnv, NODE_ENV: 'production', IS_E2E: 'true' } + renderSettings() + expect(screen.getByTestId('debug-settings-tab')).toBeInTheDocument() + }) + + it('hides the debug tab in production without IS_E2E', () => { + process.env = { ...originalEnv, NODE_ENV: 'production', IS_E2E: undefined } + renderSettings() + expect(screen.queryByTestId('debug-settings-tab')).not.toBeInTheDocument() + }) + }) + + // ── Closed state ────────────────────────────────────────────────────────── + + describe('closed state', () => { + it('does not mount main drawer content when open is false', () => { + // MUI Drawer unmounts its children when open=false (no keepMounted), + // so queryByText returns null rather than a hidden element. + renderSettings({ open: false }) + expect(screen.getByText('Community Settings')).not.toBeVisible() + }) + }) +}) diff --git a/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx b/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx index b23486e960..6483964203 100644 --- a/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx +++ b/packages/desktop/src/renderer/components/Settings/SettingsComponent.tsx @@ -14,6 +14,16 @@ const classes = { leaveComunity: `${PREFIX}leaveCommunity`, } +const TAB_TITLES: Record = { + about: 'About Quiet', + notifications: 'Notifications', + attachments: 'Files and Images', + invite: 'Add Members', + qrcode: 'QR Code', + leaveCommunity: 'Leave community', + debug: 'Debug Information', +} + export interface SettingsComponentProps { open: boolean handleClose: () => void @@ -47,16 +57,12 @@ export const SettingsComponent: React.FC = ({ return ( <> - - - -
- - - - - -
+ + + + + + Community Settings @@ -102,7 +108,7 @@ export const SettingsComponent: React.FC = ({ className={classes.leaveComunity} onClick={() => handleChange('leaveCommunity')} > - Leave community + Leave community @@ -118,17 +124,29 @@ export const SettingsComponent: React.FC = ({ )} - - - {currentTab !== '' ? : } - - - - {TabComponent && } + theme.zIndex.modal + 1 }} + > + + + + + + + + {TAB_TITLES[currentTab]} + + + + + + {TabComponent && } + diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx index 15855d883a..f693acad5a 100644 --- a/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx +++ b/packages/desktop/src/renderer/components/Settings/Tabs/Attachments/AttachmentsComponent.tsx @@ -154,11 +154,6 @@ export const AttachmentsComponent: React.FC = ({ maxAutodownlo return ( - - - Files and Images - - Auto-download... diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx index 0b654a46e5..6b09aa2a24 100644 --- a/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx +++ b/packages/desktop/src/renderer/components/Settings/Tabs/Invite/Invite.component.tsx @@ -99,13 +99,6 @@ export const InviteComponent: FC = ({ } return ( - - - - Add Members - - - Your community link diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx index ef8130bfa8..85c9182798 100644 --- a/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx +++ b/packages/desktop/src/renderer/components/Settings/Tabs/Notifications/NotificationsComponent.tsx @@ -121,11 +121,6 @@ export const NotificationsComponent: React.FC = ({ }) => { return ( - - - Notifications - - Notify me about... diff --git a/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx b/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx index ba6c1a22df..f08b990324 100644 --- a/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx +++ b/packages/desktop/src/renderer/components/Settings/Tabs/QRCode/QRCode.component.tsx @@ -34,10 +34,7 @@ export const QRCodeComponent: FC = ({ value }) => { return ( - Only admins can invite new members - - - + Only admins can invite new members to this community. Ask the community creator for a QR code to share. @@ -45,22 +42,19 @@ export const QRCodeComponent: FC = ({ value }) => { ) } return ( - - - - - - - - Invitation QR code - - - - This community QR code is private. If it is shared with someone, they can scan it with their camera to - join this community. - - - + + + + + + + This community QR code is private. If it is shared with someone, they can scan it with their camera to join + this community. + ) diff --git a/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx b/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx index cf52edc997..be0262fc88 100644 --- a/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx +++ b/packages/desktop/src/renderer/components/debugInfo/debugInfoComponent.tsx @@ -204,12 +204,6 @@ export const DebugInfoComponent: React.FC = () => { return ( - - - Debug Information - - - Summary diff --git a/packages/desktop/src/renderer/components/widgets/Settings/About.tsx b/packages/desktop/src/renderer/components/widgets/Settings/About.tsx index 6be554d6f3..b5d3d45e8e 100644 --- a/packages/desktop/src/renderer/components/widgets/Settings/About.tsx +++ b/packages/desktop/src/renderer/components/widgets/Settings/About.tsx @@ -24,19 +24,12 @@ export const About: FC = () => { const version = app.getVersion() return ( - - - About Quiet - - - - - Version: {version} -
- Copyright © {new Date().getFullYear()} Quiet LLC and other contributors -
-
+ + Version: {version} +
+ Copyright © {new Date().getFullYear()} Quiet LLC and other contributors +
)