mattermost-mobile-upstream/app/components/connection_banner/use_connection_banner.test.ts
Elias Nahum b6fc85385e
Migrate from RNN to Expo router (#9402)
* Expo-Router partial migration

* Full migration to ExpoRouter

* i18n

* add tests

* Merge branch 'main' into expo-router

* align package-lock.json

* i18n

* feedback review

* feedback review

* android 16kb patch

* Refactored emoji picker keyboard using a state machine

* set android launch_screen in drawables instead of layout

* no conditional hooks

* remove duplicate call in test

* address feedback

* fix tests and address more feedback

* small corrections

* small corrections

* small corrections

* fix draft test

* fix draft test 2

* remove navigateToScreen in draff test

* fix nit in draft test

* remove doc file

* Feedback review

* fix snapshots

* improve test coverage

* fix i18n

* Upgrade t o RN 0.83, other dependencies and new architecture (#9737)

* library hardware-keyboard android new arch support fix

* library rnshare android new arch support fix

* library rnutils android new arch support fix

* library secure-pdf-viewer add new arch support

* Enable new arch on iOS and Android

* update emm and network client with new-arch

* update intune to use new-arch

* performance improvements

* fix tests

* fixes post input on new-arch

* fix android emoji picker

* remove doc

* run tests in parallel

* upgrade to React Native 0.83.9, Expo 55, React 19, and New Architecture

Bumps core dependencies to their latest versions:
- React Native 0.77.3 → 0.83.9 (New Architecture enabled)
- React 18.3.1 → 19.2.5
- Expo SDK 52 → 55
- TypeScript 5.8.3 → 6.0.3
- Node.js 22.16.0 → 24.15.0
- react-native-reanimated 3.17.3 → 4.3.0
- Various other dependency upgrades

Migrates iOS AppDelegate from Objective-C to Swift, updates Android
build tooling and Gradle wrapper, refreshes all patches and font
assets, and adapts app code, tests, and CI workflows throughout for
API changes introduced by these upgrades.

* remove unecessary dismissKeyboard in notifications

* feedback review

* update intune to use new-arch

* update @mattermost deps

* missing i18n string

* additional fixes

* update fastlane
2026-05-13 17:44:55 +08:00

705 lines
24 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {renderHook, act, waitFor} from '@testing-library/react-native';
import {useConnectionBanner, type UseConnectionBannerParams} from './use_connection_banner';
import type {NetInfoState} from '@react-native-community/netinfo';
import type {IntlShape} from 'react-intl';
const createMockIntl = (): IntlShape => ({
formatMessage: jest.fn(({defaultMessage}) => defaultMessage || ''),
formatDate: jest.fn(),
formatTime: jest.fn(),
formatNumber: jest.fn(),
formatPlural: jest.fn(),
formatList: jest.fn(),
formatDisplayName: jest.fn(),
} as unknown as IntlShape);
const createMockNetInfo = (isInternetReachable: boolean | null = true): NetInfoState => ({
type: 'wifi',
isConnected: true,
isInternetReachable,
details: {
ssid: 'test-network',
bssid: null,
strength: 100,
ipAddress: '192.168.1.1',
subnet: '255.255.255.0',
frequency: 2400,
linkSpeed: 100,
rxLinkSpeed: null,
txLinkSpeed: null,
isConnectionExpensive: false,
},
} as NetInfoState);
describe('useConnectionBanner', () => {
let mockIntl: IntlShape;
beforeEach(() => {
mockIntl = createMockIntl();
});
describe('initial session behavior', () => {
it('should not show disconnection banner during initial session', async () => {
const {result} = renderHook(() => useConnectionBanner({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
}));
await waitFor(() => {
expect(result.current.visible).toBe(false);
expect(result.current.bannerText).toBe('');
});
});
it('should not show connecting banner during initial session', async () => {
const {result} = renderHook(() => useConnectionBanner({
websocketState: 'connecting',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
}));
await waitFor(() => {
expect(result.current.visible).toBe(false);
});
});
it('should show internet unreachable banner even during initial session', async () => {
const {result} = renderHook(() => useConnectionBanner({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
}));
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('The server is not reachable');
});
});
});
describe('after initial session (post-first-connection)', () => {
it('should show disconnection banner after initial connection is established', async () => {
const {result, rerender} = renderHook(
({websocketState, ...rest}: UseConnectionBannerParams) => useConnectionBanner({
websocketState,
...rest,
}),
{
initialProps: {
websocketState: 'connecting',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
// First, establish connection (ends initial session)
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
// Should not show "Connection restored" on initial connection
await waitFor(() => {
expect(result.current.visible).toBe(false);
});
// Now disconnect - should show banner
act(() => {
rerender({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Unable to connect to network');
});
});
it('should show connecting banner after initial session', async () => {
const {result, rerender} = renderHook(
(props: UseConnectionBannerParams) => useConnectionBanner(props),
{
initialProps: {
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
// First connect to end initial session
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
// Disconnect
act(() => {
rerender({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
// Now go to connecting state - should show banner
act(() => {
rerender({
websocketState: 'connecting',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Connecting...');
});
});
it('should show connection restored banner on reconnection', async () => {
const {result, rerender} = renderHook(
(props: UseConnectionBannerParams) => useConnectionBanner(props),
{
initialProps: {
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
// First connect to end initial session
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(false);
});
// Disconnect
act(() => {
rerender({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Unable to connect to network');
});
// Reconnect - should show "Connection restored"
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Connection restored');
expect(result.current.isShowingConnectedBanner).toBe(true);
});
});
});
describe('slow network state', () => {
it('should show slow network banner when network is slow', async () => {
const {result} = renderHook(() => useConnectionBanner({
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
}));
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Limited network connection');
});
});
it('should only show slow network banner once', () => {
jest.useFakeTimers();
const {result, rerender} = renderHook(
({networkPerformanceState, ...rest}: UseConnectionBannerParams) => useConnectionBanner({
networkPerformanceState,
...rest,
}),
{
initialProps: {
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
expect(result.current.visible).toBe(true);
// Wait for auto-close
act(() => {
jest.advanceTimersByTime(2100);
});
expect(result.current.visible).toBe(false);
// Go to normal then back to slow
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
// Should not show again
expect(result.current.visible).toBe(false);
jest.useRealTimers();
});
});
describe('banner priorities', () => {
it('should prioritize internet unreachable over disconnected', async () => {
const {result} = renderHook(() => useConnectionBanner({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
}));
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('The server is not reachable');
});
});
it('should not re-show banner the moment it closes (effect omits visible from deps)', () => {
jest.useFakeTimers();
const {result} = renderHook(() => useConnectionBanner({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
}));
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('The server is not reachable');
// Auto-close: the hook sets a 2s timeout when showing internet unreachable. Advance
// time so closeCallback runs. visible becomes false; netInfo is still unreachable.
act(() => {
jest.advanceTimersByTime(2100);
});
expect(result.current.visible).toBe(false);
jest.useRealTimers();
});
it('should not show other banners when one is already visible with timeout', async () => {
const {result, rerender} = renderHook(
(props: UseConnectionBannerParams) => useConnectionBanner(props),
{
initialProps: {
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
},
},
);
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('The server is not reachable');
});
// Try to trigger slow network while banner is visible
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
});
});
// Should still show internet unreachable
await waitFor(() => {
expect(result.current.bannerText).toBe('The server is not reachable');
});
});
});
describe('app state changes', () => {
it('should hide banner when app goes to background', async () => {
const {result, rerender} = renderHook(
({appState, ...rest}: UseConnectionBannerParams) => useConnectionBanner({
appState,
...rest,
}),
{
initialProps: {
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
},
},
);
await waitFor(() => {
expect(result.current.visible).toBe(true);
});
// Go to background
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'background',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(false);
expect(result.current.bannerText).toBe('');
});
});
it('should reset slow banner flag when app goes to background', async () => {
const {result, rerender} = renderHook(
({appState, networkPerformanceState, ...rest}: UseConnectionBannerParams) => useConnectionBanner({
appState,
networkPerformanceState,
...rest,
}),
{
initialProps: {
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
await waitFor(() => {
expect(result.current.visible).toBe(true);
});
// Go to background
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'background',
intl: mockIntl,
});
});
// Come back to active
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
// Should show slow banner again (flag was reset)
await waitFor(() => {
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Limited network connection');
});
});
it('should not show connection restored banner when returning from background if websocket stayed connected', async () => {
const {result, rerender} = renderHook(
({appState, websocketState, ...rest}: UseConnectionBannerParams) => useConnectionBanner({
appState,
websocketState,
...rest,
}),
{
initialProps: {
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(false);
});
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'background',
intl: mockIntl,
});
});
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
await waitFor(() => {
expect(result.current.visible).toBe(false);
expect(result.current.bannerText).toBe('');
expect(result.current.isShowingConnectedBanner).toBe(false);
});
});
});
describe('auto-close behavior', () => {
it('should auto-close internet unreachable banner after 2 seconds', () => {
jest.useFakeTimers();
const {result, rerender} = renderHook(
(props: UseConnectionBannerParams) => useConnectionBanner(props),
{
initialProps: {
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(false),
appState: 'active',
intl: mockIntl,
},
},
);
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('The server is not reachable');
// Internet becomes reachable again, then wait for timeout
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(true),
appState: 'active',
intl: mockIntl,
});
jest.advanceTimersByTime(2100);
});
expect(result.current.visible).toBe(false);
jest.useRealTimers();
});
it('should auto-close slow network banner after 2 seconds', () => {
jest.useFakeTimers();
const {result, rerender} = renderHook(
(props: UseConnectionBannerParams) => useConnectionBanner(props),
{
initialProps: {
websocketState: 'connected',
networkPerformanceState: 'slow',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Limited network connection');
// Network becomes normal, then wait for timeout
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
jest.advanceTimersByTime(2100);
});
expect(result.current.visible).toBe(false);
jest.useRealTimers();
});
it('should auto-close connection restored banner after 2 seconds', () => {
jest.useFakeTimers();
const {result, rerender} = renderHook(
(props: UseConnectionBannerParams) => useConnectionBanner(props),
{
initialProps: {
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
},
},
);
// First connect to end initial session
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
expect(result.current.visible).toBe(false);
// Disconnect
act(() => {
rerender({
websocketState: 'not_connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Unable to connect to network');
// Reconnect - should show "Connection restored"
act(() => {
rerender({
websocketState: 'connected',
networkPerformanceState: 'normal',
netInfo: createMockNetInfo(),
appState: 'active',
intl: mockIntl,
});
});
expect(result.current.visible).toBe(true);
expect(result.current.bannerText).toBe('Connection restored');
expect(result.current.isShowingConnectedBanner).toBe(true);
// Wait for auto-close
act(() => {
jest.advanceTimersByTime(2100);
});
expect(result.current.visible).toBe(false);
expect(result.current.isShowingConnectedBanner).toBe(false);
jest.useRealTimers();
});
});
});