Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div data-testid='mock-tab-content'>
<p>Tab content</p>
<button onClick={handleClose}>Close from tab</button>
</div>
)

const defaultTabs: Record<string, React.FC<{ handleClose: () => void }>> = {
about: MockTab,
notifications: MockTab,
attachments: MockTab,
invite: MockTab,
qrcode: MockTab,
leaveCommunity: MockTab,
debug: MockTab,
}

function renderSettings(overrides: Partial<React.ComponentProps<typeof SettingsComponent>> = {}) {
return render(
<SettingsComponent
open={true}
handleClose={noOp}
tabs={defaultTabs}
leaveCommunityModal={mockLeaveCommunityModal}
{...overrides}
/>
)
}

// 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()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ const classes = {
leaveComunity: `${PREFIX}leaveCommunity`,
}

const TAB_TITLES: Record<string, string> = {
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
Expand Down Expand Up @@ -47,16 +57,12 @@ export const SettingsComponent: React.FC<SettingsComponentProps> = ({

return (
<>
<Drawer open={open} onClose={handleClose} anchor='right'>
<List sx={{ width: '375px', paddingTop: '16px' }}>
<ListItem sx={{ paddingBottom: '8px' }}>
<div>
<ListItemButton onClick={handleClose} sx={{ padding: '0px' }} data-testid={'close-settings-button'}>
<ListItemIcon>
<CloseIcon />
</ListItemIcon>
</ListItemButton>
</div>
<Drawer open={open} onClose={handleClose} anchor='right' ModalProps={{ keepMounted: true }}>
<List sx={{ width: '375px', paddingTop: '0px' }}>
<ListItem sx={{ paddingTop: '12px', paddingBottom: '12px' }}>
<IconButton onClick={handleClose} data-testid={'close-settings-button'}>
<CloseIcon />
</IconButton>
<ListItemText sx={{ textAlign: 'center' }}>
<Typography sx={{ fontWeight: '500' }}>Community Settings</Typography>
</ListItemText>
Expand Down Expand Up @@ -102,7 +108,7 @@ export const SettingsComponent: React.FC<SettingsComponentProps> = ({
className={classes.leaveComunity}
onClick={() => handleChange('leaveCommunity')}
>
<ListItemText>Leave community</ListItemText>
<ListItemText sx={{ color: 'error.main' }}>Leave community</ListItemText>
<ListItemIcon>
<ChevronRightIcon />
</ListItemIcon>
Expand All @@ -118,17 +124,29 @@ export const SettingsComponent: React.FC<SettingsComponentProps> = ({
)}
</List>
</Drawer>
<Drawer open={currentTab !== ''} onClose={handleCloseTab} anchor='right' BackdropProps={{ invisible: true }}>
<Box
width={40}
sx={{ paddingTop: '16px', paddingBottom: '8px', paddingLeft: '4px' }}
data-testid={'close-tab-button-box'}
>
<IconButton onClick={handleCloseTab}>{currentTab !== '' ? <ArrowBackIcon /> : <CloseIcon />}</IconButton>
</Box>
<Divider />
<Box p={2} width={375}>
{TabComponent && <TabComponent handleClose={handleCloseTab} />}
<Drawer
open={currentTab !== ''}
onClose={handleCloseTab}
anchor='right'
BackdropProps={{ invisible: true }}
ModalProps={{ keepMounted: true, disablePortal: true }}
sx={{ zIndex: theme => theme.zIndex.modal + 1 }}
>
<Box display='flex' flexDirection='column' height='100%' width={375}>
<Box>
<ListItem sx={{ paddingTop: '12px', paddingBottom: '12px' }}>
<IconButton onClick={handleCloseTab} data-testid={'close-tab-button-box'}>
<ArrowBackIcon />
</IconButton>
<ListItemText sx={{ textAlign: 'center' }}>
<Typography sx={{ fontWeight: 500 }}>{TAB_TITLES[currentTab]}</Typography>
</ListItemText>
</ListItem>
<Divider />
</Box>
<Box p={2} flex={1} overflow='auto'>
{TabComponent && <TabComponent handleClose={handleCloseTab} />}
</Box>
</Box>
</Drawer>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,6 @@ export const AttachmentsComponent: React.FC<AttachmentsProps> = ({ maxAutodownlo

return (
<StyledGrid container direction='column'>
<Grid container item justifyContent='space-between' alignItems='center' className={classes.titleDiv}>
<Grid item className={classes.title}>
<Typography variant='h3'>Files and Images</Typography>
</Grid>
</Grid>
<Grid item>
<Typography variant='h5' className={classes.subtitle}>
Auto-download...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,6 @@ export const InviteComponent: FC<InviteComponentProps> = ({
}
return (
<StyledGrid container direction='column'>
<Grid container item justifyContent='space-between' alignItems='center' className={classes.titleDiv}>
<Grid item className={classes.title}>
<Typography variant='h3' data-testid='invite-a-friend'>
Add Members
</Typography>
</Grid>
</Grid>
<Grid item className={classes.wrapper}>
<Grid item>
<Typography variant='h5'>Your community link</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,6 @@ export const NotificationsComponent: React.FC<NotificationsProps> = ({
}) => {
return (
<StyledGrid container direction='column'>
<Grid container item justifyContent='space-between' alignItems='center' className={classes.titleDiv}>
<Grid item className={classes.title}>
<Typography variant='h3'>Notifications</Typography>
</Grid>
</Grid>
<Grid item>
<Typography variant='h5' className={classes.subtitle}>
Notify me about...
Expand Down
Loading