Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -89,7 +89,7 @@ describe('BrainLeftSidebarWave', () => {
render(<BrainLeftSidebarWave wave={baseWave} onHover={onHover} />);
const link = screen.getByRole('link');
await userEvent.click(link);
expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false, serialNo: null, divider: null });
expect(setActiveWave).toHaveBeenCalledWith('1', { isDirectMessage: false, divider: null });
});

it('shows drop indicators for non-chat waves', () => {
Expand All @@ -101,7 +101,7 @@ describe('BrainLeftSidebarWave', () => {
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?divider=42&wave=3&serialNo=42');
expect(screen.getByRole('link')).toHaveAttribute('href', '/waves?divider=42&wave=3');
});

it('does not include serialNo in href when firstUnreadDropSerialNo is null', () => {
Expand Down
29 changes: 19 additions & 10 deletions __tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { render, screen, act } from "@testing-library/react";
import MyStreamWaveChat from "@/components/brain/my-stream/MyStreamWaveChat";
import { editSlice } from "@/store/editSlice";
import { configureStore } from "@reduxjs/toolkit";
import { act, render, screen } from "@testing-library/react";
import React from "react";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { editSlice } from "@/store/editSlice";
import MyStreamWaveChat from "@/components/brain/my-stream/MyStreamWaveChat";

const replaceMock = jest.fn();
const searchParamsMock = { get: jest.fn() };
Expand All @@ -23,11 +23,15 @@ jest.mock("@/components/brain/my-stream/layout/LayoutContext", () => ({
useLayout: () => ({ waveViewStyle: { height: "1px" } }),
}));

let capturedProps: any = {};
const capturedPropsHolder = { current: {} as any };
jest.mock("@/components/waves/drops/wave-drops-all", () => ({
__esModule: true,
default: (props: any) => {
capturedProps = props;
capturedPropsHolder.current = props;
return <div data-testid="drops" />;
},
WaveDropsAllWithoutProvider: (props: any) => {
capturedPropsHolder.current = props;
return <div data-testid="drops" />;
},
}));
Expand Down Expand Up @@ -55,21 +59,26 @@ jest.mock("@/hooks/useDeviceInfo", () => ({
default: () => ({ isApp: false }),
}));

jest.mock("@/contexts/wave/UnreadDividerContext", () => ({
UnreadDividerProvider: ({ children }: any) => <>{children}</>,
}));

jest.mock("@/components/waves/gallery", () => ({
WaveGallery: () => <div data-testid="gallery" />,
}));

const wave = { id: "10" } as any;
const wave = { id: "10", metrics: { muted: false } } as any;
const mockOnDropClick = jest.fn();

describe("MyStreamWaveChat", () => {
let store: any;

beforeEach(() => {
capturedProps = {};
capturedPropsHolder.current = {};
replaceMock.mockClear();
searchParamsMock.get.mockReset();
mockIsMemesWave = false;
mockOnDropClick.mockClear();
store = configureStore({
reducer: { edit: editSlice.reducer },
});
Expand All @@ -93,7 +102,7 @@ describe("MyStreamWaveChat", () => {
);
});
expect(replaceMock).toHaveBeenCalled();
expect(capturedProps.initialDrop).toBe(5);
expect(capturedPropsHolder.current.initialDrop).toBe(5);
expect(screen.getByTestId("memes-btn")).toBeInTheDocument();
});

Expand All @@ -110,7 +119,7 @@ describe("MyStreamWaveChat", () => {
);
});
expect(replaceMock).not.toHaveBeenCalled();
expect(capturedProps.initialDrop).toBeNull();
expect(capturedPropsHolder.current.initialDrop).toBeNull();
expect(screen.queryByTestId("memes-btn")).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DropMobileMenuHandler from '@/components/waves/drops/DropMobileMenuHandle
import { DropSize } from '@/helpers/waves/drop.helpers';

jest.mock('@/hooks/isMobileDevice', () => () => true);
jest.mock('@/hooks/useIsTouchDevice', () => ({ __esModule: true, default: () => true }));

jest.mock('@/components/waves/drops/WaveDropMobileMenu', () => ({
__esModule: true,
Expand Down
1 change: 1 addition & 0 deletions __tests__/components/waves/drops/WaveDrop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jest.mock('@/components/waves/drops/WaveDropRatings', () => () => <div data-test
jest.mock('@/components/waves/drops/WaveDropMobileMenu', () => () => <div data-testid="mobile" />);

jest.mock('@/hooks/isMobileDevice');
jest.mock('@/hooks/useIsTouchDevice', () => ({ __esModule: true, default: jest.fn(() => false) }));

jest.mock('next/navigation', () => ({
useRouter: jest.fn(() => ({ push: jest.fn() })),
Expand Down
5 changes: 5 additions & 0 deletions __tests__/components/waves/drops/WaveDropsAll.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ jest.mock('@/components/waves/drops/WaveDropsScrollBottomButton', () => ({
}
}));

jest.mock('@/components/waves/drops/WaveDropsScrollToUnreadButton', () => ({
__esModule: true,
WaveDropsScrollToUnreadButton: () => <button data-testid="scroll-unread-btn" />
}));

jest.mock('@/components/waves/drops/WaveDropsEmptyPlaceholder', () => ({
__esModule: true,
default: () => <div data-testid="empty-placeholder" />
Expand Down
65 changes: 56 additions & 9 deletions __tests__/hooks/useDeviceInfo.test.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,97 @@
import { renderHook } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react';
import useDeviceInfo from '@/hooks/useDeviceInfo';

jest.mock('@/hooks/useCapacitor', () => ({ __esModule: true, default: jest.fn(() => ({ isCapacitor: false })) }));
const capacitorMock = require('@/hooks/useCapacitor').default as jest.Mock;

defineMatchMedia();
let touchStartHandler: EventListener | null = null;

function defineMatchMedia(pointer = false, width = false) {
function defineMatchMedia(pointerFine = true, width = false) {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn((query) => ({
matches: query.includes('pointer') ? pointer : width,
value: jest.fn((query: string) => ({
matches: query === '(pointer: fine)' ? pointerFine : query === '(max-width: 768px)' ? width : false,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
})),
});
}

describe('useDeviceInfo', () => {
beforeEach(() => {
jest.spyOn(window, 'addEventListener').mockImplementation((event, handler) => {
if (event === 'touchstart') {
touchStartHandler = handler as EventListener;
}
});
jest.spyOn(window, 'removeEventListener');
});

afterEach(() => {
touchStartHandler = null;
jest.restoreAllMocks();
capacitorMock.mockReturnValue({ isCapacitor: false });
});

it('detects classic mobile user agent', () => {
Object.defineProperty(window.navigator, 'userAgent', { value: 'iPhone', configurable: true });
defineMatchMedia(true, false);
defineMatchMedia(false, false);
const { result } = renderHook(() => useDeviceInfo());
expect(result.current.isMobileDevice).toBe(true);
expect(result.current.isAppleMobile).toBe(true);
expect(result.current.hasTouchScreen).toBe(true);
expect(result.current.isApp).toBe(false);
expect(result.current.hasTouchScreen).toBe(false);

act(() => {
if (touchStartHandler) {
touchStartHandler(new Event('touchstart'));
}
});

expect(result.current.hasTouchScreen).toBe(true);
});

it('detects capacitor mobile with desktop UA', () => {
capacitorMock.mockReturnValue({ isCapacitor: true });
Object.defineProperty(window.navigator, 'userAgent', { value: 'Macintosh', configurable: true });
defineMatchMedia(true, true);
defineMatchMedia(false, true);
const { result } = renderHook(() => useDeviceInfo());
expect(result.current.isMobileDevice).toBe(true);
expect(result.current.isApp).toBe(true);
expect(result.current.isAppleMobile).toBe(false);

act(() => {
if (touchStartHandler) {
touchStartHandler(new Event('touchstart'));
}
});

expect(result.current.isAppleMobile).toBe(true);
expect(result.current.hasTouchScreen).toBe(true);
});

it('returns false for desktop without touch', () => {
capacitorMock.mockReturnValue({ isCapacitor: false });
Object.defineProperty(window.navigator, 'userAgent', { value: 'Mozilla/5.0', configurable: true });
defineMatchMedia(false, false);
defineMatchMedia(true, false);
const { result } = renderHook(() => useDeviceInfo());
expect(result.current.isMobileDevice).toBe(false);
expect(result.current.hasTouchScreen).toBe(false);
expect(result.current.isAppleMobile).toBe(false);
});

it('hasTouchScreen stays false when fine pointer is present even after touch', () => {
capacitorMock.mockReturnValue({ isCapacitor: false });
Object.defineProperty(window.navigator, 'userAgent', { value: 'Mozilla/5.0', configurable: true });
defineMatchMedia(true, false);
const { result } = renderHook(() => useDeviceInfo());

act(() => {
if (touchStartHandler) {
touchStartHandler(new Event('touchstart'));
}
});

expect(result.current.hasTouchScreen).toBe(false);
});
});
118 changes: 49 additions & 69 deletions __tests__/hooks/useIsTouchDevice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,94 @@ import { renderHook, act } from "@testing-library/react";
import useIsTouchDevice from "@/hooks/useIsTouchDevice";

describe("useIsTouchDevice", () => {
const originalWindow = globalThis.window;
const originalNavigator = globalThis.navigator;
let addEventListenerSpy: jest.SpyInstance;
let removeEventListenerSpy: jest.SpyInstance;
let touchStartHandler: EventListener | null = null;

beforeEach(() => {
addEventListenerSpy = jest.spyOn(globalThis, "addEventListener").mockImplementation((event, handler) => {
if (event === "touchstart") {
touchStartHandler = handler as EventListener;
}
});
removeEventListenerSpy = jest.spyOn(globalThis, "removeEventListener");
});

afterEach(() => {
Object.defineProperty(globalThis, "window", {
value: originalWindow,
writable: true,
});
Object.defineProperty(globalThis, "navigator", {
value: originalNavigator,
writable: true,
});
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
touchStartHandler = null;
jest.restoreAllMocks();
});

it("returns false when window is undefined", () => {
Object.defineProperty(globalThis, "window", {
value: undefined,
it("returns false initially when fine pointer is detected", () => {
Object.defineProperty(globalThis, "matchMedia", {
writable: true,
value: jest.fn((query: string) => ({
matches: query === "(pointer: fine)",
})),
});

const { result } = renderHook(() => useIsTouchDevice());
expect(result.current).toBe(false);
});

it("returns true when ontouchstart is in window", () => {
const mockWindow = {
...originalWindow,
ontouchstart: null,
matchMedia: jest.fn(() => ({ matches: false })),
};
Object.defineProperty(globalThis, "window", {
value: mockWindow,
it("returns false initially and does not listen for touch when fine pointer exists", () => {
Object.defineProperty(globalThis, "matchMedia", {
writable: true,
value: jest.fn((query: string) => ({
matches: query === "(pointer: fine)",
})),
});

const { result } = renderHook(() => useIsTouchDevice());

act(() => {
jest.runAllTimers?.();
});

expect(result.current).toBe(true);
renderHook(() => useIsTouchDevice());
expect(addEventListenerSpy).not.toHaveBeenCalledWith("touchstart", expect.any(Function), expect.any(Object));
});

it("returns true when navigator.maxTouchPoints > 0", () => {
const mockWindow = {
...originalWindow,
matchMedia: jest.fn(() => ({ matches: false })),
};
Object.defineProperty(globalThis, "window", {
value: mockWindow,
writable: true,
});
Object.defineProperty(globalThis, "navigator", {
value: { maxTouchPoints: 5 },
it("returns false initially but switches to true after touchstart when no fine pointer", () => {
Object.defineProperty(globalThis, "matchMedia", {
writable: true,
value: jest.fn(() => ({ matches: false })),
});

const { result } = renderHook(() => useIsTouchDevice());
expect(result.current).toBe(false);

act(() => {
jest.runAllTimers?.();
if (touchStartHandler) {
touchStartHandler(new Event("touchstart"));
}
});

expect(result.current).toBe(true);
});

it("returns true when matchMedia pointer:coarse matches", () => {
const mockWindow = {
...originalWindow,
matchMedia: jest.fn((query: string) => ({
matches: query === "(pointer: coarse)",
})),
};
Object.defineProperty(globalThis, "window", {
value: mockWindow,
writable: true,
});
Object.defineProperty(globalThis, "navigator", {
value: { maxTouchPoints: 0 },
it("removes touchstart listener after first touch", () => {
Object.defineProperty(globalThis, "matchMedia", {
writable: true,
value: jest.fn(() => ({ matches: false })),
});

const { result } = renderHook(() => useIsTouchDevice());
renderHook(() => useIsTouchDevice());

act(() => {
jest.runAllTimers?.();
if (touchStartHandler) {
touchStartHandler(new Event("touchstart"));
}
});

expect(result.current).toBe(true);
expect(removeEventListenerSpy).toHaveBeenCalledWith("touchstart", expect.any(Function));
});

it("returns false when no touch indicators present", () => {
const mockWindow = {
...originalWindow,
matchMedia: jest.fn(() => ({ matches: false })),
};
Object.defineProperty(globalThis, "window", {
value: mockWindow,
writable: true,
});
Object.defineProperty(globalThis, "navigator", {
value: { maxTouchPoints: 0 },
it("cleans up event listener on unmount", () => {
Object.defineProperty(globalThis, "matchMedia", {
writable: true,
value: jest.fn(() => ({ matches: false })),
});

const { result } = renderHook(() => useIsTouchDevice());
const { unmount } = renderHook(() => useIsTouchDevice());
unmount();

expect(result.current).toBe(false);
expect(removeEventListenerSpy).toHaveBeenCalledWith("touchstart", expect.any(Function));
});
});
Loading