Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ describe('BrainLeftSidebarWave', () => {
contributors: [],
newDropsCount: { count: 2, latestDropTimestamp: 123 },
isPinned: false,
unreadDropsCount: 0,
latestReadTimestamp: 0,
firstUnreadDropSerialNo: null,
isMuted: false,
} as any;

beforeEach(() => {
Expand Down Expand Up @@ -85,12 +89,38 @@ describe('BrainLeftSidebarWave', () => {
render(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
const link = screen.getByRole('link');
await userEvent.click(link);
expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false });
expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false, serialNo: null });
});

it('shows drop indicators for non-chat waves', () => {
const dropWave = { ...baseWave, id: '2', type: ApiWaveType.Approve };
render(<BrainLeftSidebarWave wave={dropWave} onHover={onHover} />);
expect(screen.getByTestId('drop-time')).toHaveTextContent('123');
});

it('includes firstUnreadDropSerialNo in href when present', () => {
const waveWithUnread = { ...baseWave, id: '3', firstUnreadDropSerialNo: 42 };
render(<BrainLeftSidebarWave wave={waveWithUnread} onHover={onHover} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/waves?wave=3&serialNo=42');
});

it('does not include serialNo in href when firstUnreadDropSerialNo is null', () => {
const waveWithoutUnread = { ...baseWave, id: '4', firstUnreadDropSerialNo: null };
render(<BrainLeftSidebarWave wave={waveWithoutUnread} onHover={onHover} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/waves?wave=4');
});

it('shows muted indicator when wave is muted', () => {
const mutedWave = { ...baseWave, id: '5', isMuted: true };
render(<BrainLeftSidebarWave wave={mutedWave} onHover={onHover} />);
const bellSlashIcons = document.querySelectorAll('[data-icon="bell-slash"]');
expect(bellSlashIcons.length).toBeGreaterThan(0);
});

it('does not show muted indicator when wave is not muted', () => {
const unmutedWave = { ...baseWave, id: '6', isMuted: false };
render(<BrainLeftSidebarWave wave={unmutedWave} onHover={onHover} />);
const bellSlashIcons = document.querySelectorAll('[data-icon="bell-slash"]');
expect(bellSlashIcons.length).toBe(0);
});
});
93 changes: 93 additions & 0 deletions __tests__/components/drops/view/DropsList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ jest.mock("@/components/drops/view/HighlightDropWrapper", () => ({
},
}));

jest.mock("@/components/drops/view/UnreadDivider", () => ({
__esModule: true,
default: () => <div data-testid="unread-divider" />,
}));

describe("DropsList", () => {
beforeEach(() => {
dropProps = [];
Expand Down Expand Up @@ -84,4 +89,92 @@ describe("DropsList", () => {
expect(dropProps).toHaveLength(1);
expect(lightProps).toHaveLength(1);
});

it("renders unread divider when unreadDividerSerialNo matches a drop", () => {
const drops: any = [
{ stableKey: "a", serial_no: 1, type: DropSize.FULL, wave: { id: "w" } },
{ stableKey: "b", serial_no: 2, type: DropSize.FULL, wave: { id: "w" } },
{ stableKey: "c", serial_no: 3, type: DropSize.FULL, wave: { id: "w" } },
];

render(
<DropsList
scrollContainerRef={{ current: null }}
drops={drops}
showWaveInfo={false}
activeDrop={null}
showReplyAndQuote={false}
onReply={jest.fn()}
onQuote={jest.fn()}
onReplyClick={jest.fn()}
serialNo={null}
targetDropRef={null}
parentContainerRef={undefined}
onQuoteClick={jest.fn()}
onDropContentClick={jest.fn()}
dropViewDropId={null}
unreadDividerSerialNo={2}
/>
);

expect(screen.getByTestId("unread-divider")).toBeInTheDocument();
});

it("does not render unread divider when unreadDividerSerialNo is null", () => {
const drops: any = [
{ stableKey: "a", serial_no: 1, type: DropSize.FULL, wave: { id: "w" } },
{ stableKey: "b", serial_no: 2, type: DropSize.FULL, wave: { id: "w" } },
];

render(
<DropsList
scrollContainerRef={{ current: null }}
drops={drops}
showWaveInfo={false}
activeDrop={null}
showReplyAndQuote={false}
onReply={jest.fn()}
onQuote={jest.fn()}
onReplyClick={jest.fn()}
serialNo={null}
targetDropRef={null}
parentContainerRef={undefined}
onQuoteClick={jest.fn()}
onDropContentClick={jest.fn()}
dropViewDropId={null}
unreadDividerSerialNo={null}
/>
);

expect(screen.queryByTestId("unread-divider")).not.toBeInTheDocument();
});

it("does not render unread divider when unreadDividerSerialNo does not match any drop", () => {
const drops: any = [
{ stableKey: "a", serial_no: 1, type: DropSize.FULL, wave: { id: "w" } },
{ stableKey: "b", serial_no: 2, type: DropSize.FULL, wave: { id: "w" } },
];

render(
<DropsList
scrollContainerRef={{ current: null }}
drops={drops}
showWaveInfo={false}
activeDrop={null}
showReplyAndQuote={false}
onReply={jest.fn()}
onQuote={jest.fn()}
onReplyClick={jest.fn()}
serialNo={null}
targetDropRef={null}
parentContainerRef={undefined}
onQuoteClick={jest.fn()}
onDropContentClick={jest.fn()}
dropViewDropId={null}
unreadDividerSerialNo={999}
/>
);

expect(screen.queryByTestId("unread-divider")).not.toBeInTheDocument();
});
});
21 changes: 21 additions & 0 deletions __tests__/components/drops/view/UnreadDivider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react';
import UnreadDivider from '@/components/drops/view/UnreadDivider';

describe('UnreadDivider', () => {
it('renders with default label', () => {
render(<UnreadDivider />);
expect(screen.getByText('New Messages')).toBeInTheDocument();
});

it('renders with custom label', () => {
render(<UnreadDivider label="Unread Items" />);
expect(screen.getByText('Unread Items')).toBeInTheDocument();
});

it('renders horizontal lines', () => {
const { container } = render(<UnreadDivider />);
const lines = container.querySelectorAll(String.raw`.tw-h-0\.5.tw-bg-rose-500`);
expect(lines.length).toBe(2);
});
});

1 change: 1 addition & 0 deletions __tests__/components/waves/drops/WaveDropActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jest.mock('@/components/waves/drops/WaveDropActionsOptions', () => () => <div da
jest.mock('@/components/waves/drops/WaveDropActionsOpen', () => () => <div data-testid="open" />);
jest.mock('@/components/waves/drops/WaveDropFollowAuthor', () => () => <div data-testid="follow" />);
jest.mock('@/components/waves/drops/WaveDropActionsAddReaction', () => () => <div data-testid="add-reaction" />);
jest.mock('@/components/waves/drops/WaveDropActionsMarkUnread', () => () => <div data-testid="mark-unread" />);

jest.mock('@/hooks/drops/useDropInteractionRules', () => ({ useDropInteractionRules: jest.fn() }));
jest.mock('@/contexts/SeizeSettingsContext', () => ({ useSeizeSettings: jest.fn() }));
Expand Down
150 changes: 150 additions & 0 deletions __tests__/components/waves/drops/WaveDropActionsMarkUnread.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import WaveDropActionsMarkUnread from '@/components/waves/drops/WaveDropActionsMarkUnread';
import { AuthContext } from '@/components/auth/Auth';
import { ApiDrop } from '@/generated/models/ApiDrop';

jest.mock('@/services/api/common-api', () => ({
commonApiPost: jest.fn(),
}));

jest.mock('@tanstack/react-query', () => ({
useQueryClient: () => ({
invalidateQueries: jest.fn(),
}),
}));

jest.mock('@/contexts/wave/UnreadDividerContext', () => ({
useUnreadDividerOptional: () => ({
setUnreadDividerSerialNo: jest.fn(),
}),
}));

jest.mock('@/contexts/wave/MyStreamContext', () => ({
useMyStream: () => ({
waves: {
restoreWaveUnreadCount: jest.fn(),
},
directMessages: {
restoreWaveUnreadCount: jest.fn(),
},
}),
}));

jest.mock('react-tooltip', () => ({
Tooltip: ({ children }: any) => <div>{children}</div>,
}));

const mockAuthContext = {
setToast: jest.fn(),
};

const mockDrop: ApiDrop = {
id: 'drop-123',
serial_no: 42,
wave: {
id: 'wave-456',
name: 'Test Wave',
},
} as any;

describe('WaveDropActionsMarkUnread', () => {
beforeEach(() => {
jest.clearAllMocks();
});

const renderComponent = () => {
return render(
<AuthContext.Provider value={mockAuthContext as any}>
<WaveDropActionsMarkUnread drop={mockDrop} />
</AuthContext.Provider>
);
};

it('renders mark unread button', () => {
renderComponent();
expect(screen.getByLabelText('Mark as unread')).toBeInTheDocument();
});

it('calls API when clicked', async () => {
const { commonApiPost } = require('@/services/api/common-api');
commonApiPost.mockResolvedValue({
your_unread_drops_count: 5,
first_unread_drop_serial_no: 42,
});

renderComponent();

await userEvent.click(screen.getByLabelText('Mark as unread'));

await waitFor(() => {
expect(commonApiPost).toHaveBeenCalledWith({
endpoint: 'drops/drop-123/mark-unread',
body: {},
});
});
});

it('shows success toast on success', async () => {
const { commonApiPost } = require('@/services/api/common-api');
commonApiPost.mockResolvedValue({
your_unread_drops_count: 5,
first_unread_drop_serial_no: 42,
});

renderComponent();

await userEvent.click(screen.getByLabelText('Mark as unread'));

await waitFor(() => {
expect(mockAuthContext.setToast).toHaveBeenCalledWith({
message: 'Marked as unread',
type: 'success',
});
});
});

it('shows error toast on failure', async () => {
const { commonApiPost } = require('@/services/api/common-api');
commonApiPost.mockRejectedValue('API Error');

renderComponent();

await userEvent.click(screen.getByLabelText('Mark as unread'));

await waitFor(() => {
expect(mockAuthContext.setToast).toHaveBeenCalledWith({
message: 'API Error',
type: 'error',
});
});
});

it('shows loading spinner while marking unread', async () => {
const { commonApiPost } = require('@/services/api/common-api');
commonApiPost.mockImplementation(() => new Promise(() => {}));

renderComponent();

await userEvent.click(screen.getByLabelText('Mark as unread'));

await waitFor(() => {
expect(screen.getByLabelText('Mark as unread').querySelector('.spinner')).toBeInTheDocument();
});
});

it('disables button while loading', async () => {
const { commonApiPost } = require('@/services/api/common-api');
commonApiPost.mockImplementation(() => new Promise(() => {}));

renderComponent();

const button = screen.getByLabelText('Mark as unread');
await userEvent.click(button);

await waitFor(() => {
expect(button).toBeDisabled();
});
});
});

4 changes: 2 additions & 2 deletions __tests__/components/waves/drops/WaveDropsAll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ describe('WaveDropsAll', () => {
describe('Navigation and Quote Handling', () => {
it('navigates to different wave when quote is from another wave', () => {
const mockDrop = createMockDrop({
wave: { id: 'other-wave', name: 'Other Wave', picture: null, description_drop_id: null },
wave: { id: 'other-wave', name: 'Other Wave', picture: null, description_drop_id: '' },
serial_no: 42
}) as any;

Expand All @@ -453,7 +453,7 @@ describe('WaveDropsAll', () => {

it('sets serial number for same wave quote navigation', () => {
const mockDrop = createMockDrop({
wave: { id: 'current-wave', name: 'Current Wave', picture: null, description_drop_id: null },
wave: { id: 'current-wave', name: 'Current Wave', picture: null, description_drop_id: '' },
serial_no: 42
}) as any;

Expand Down
17 changes: 15 additions & 2 deletions __tests__/components/waves/header/WaveHeaderOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import WaveHeaderOptions from '@/components/waves/header/options/WaveHeaderOptions';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

let clickAway: () => void; let escCb: () => void;

Expand All @@ -16,14 +17,26 @@ jest.mock('framer-motion', () => ({

jest.mock('@/components/waves/header/options/delete/WaveDelete', () => (props: any) => <div data-testid="delete" data-wave={props.wave.id} />);

const wave = { id: 'w1' } as any;
jest.mock('@/components/waves/header/options/mute/WaveMute', () => (props: any) => <div data-testid="mute" data-wave={props.wave.id} />);

const wave = { id: 'w1', metrics: { muted: false } } as any;

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

test('opens and closes options', async () => {
const user = userEvent.setup();
const { rerender } = render(<WaveHeaderOptions wave={wave} />);
const { rerender } = render(<WaveHeaderOptions wave={wave} />, { wrapper: createWrapper() });
const btn = screen.getByRole('button');
await user.click(btn);
expect(screen.getByTestId('delete')).toHaveAttribute('data-wave','w1');
expect(screen.getByTestId('mute')).toHaveAttribute('data-wave','w1');
// click away
Comment thread
prxt6529 marked this conversation as resolved.
Outdated
clickAway();
rerender(<WaveHeaderOptions wave={wave} />);
Expand Down
Loading