mattermost-mobile-upstream/app/hooks/use_keyboard_state_machine/index.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

206 lines
6.9 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {renderHook, act} from '@testing-library/react-native';
import {StateMachineEventType} from '@keyboard';
import {useKeyboardStateMachine} from './index';
jest.mock('react-native-reanimated', () => ({
...jest.requireActual('react-native-reanimated'),
}));
beforeEach(() => {
jest.useFakeTimers({doNotFake: ['nextTick']});
});
afterEach(() => {
jest.useRealTimers();
});
async function flushScheduled() {
await act(async () => {
jest.runAllTimers();
});
}
function makeContext() {
const processEvent = jest.fn();
const isEmojiPickerActive = jest.fn(() => false);
return {processEvent, isEmojiPickerActive};
}
describe('useKeyboardStateMachine', () => {
describe('onUserFocusInput', () => {
it('should dispatch USER_FOCUS_INPUT with undefined height and progress by default', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserFocusInput();
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.USER_FOCUS_INPUT,
rawHeight: undefined,
height: undefined,
progress: undefined,
});
});
it('should dispatch USER_FOCUS_INPUT with height=0 and progress=0 when asHardwareKeyboard=true', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserFocusInput(true);
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: StateMachineEventType.USER_FOCUS_INPUT,
rawHeight: 0,
height: 0,
progress: 0,
}),
);
});
it('should also dispatch KEYBOARD_EVENT_START and KEYBOARD_EVENT_END when asHardwareKeyboard=true', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserFocusInput(true);
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.KEYBOARD_EVENT_START,
rawHeight: 0,
height: 0,
progress: 0,
});
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.KEYBOARD_EVENT_END,
rawHeight: 0,
height: 0,
progress: 0,
});
expect(ctx.processEvent).toHaveBeenCalledTimes(3);
});
it('should NOT dispatch hardware keyboard events when asHardwareKeyboard=false', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserFocusInput(false);
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledTimes(1);
expect(ctx.processEvent).not.toHaveBeenCalledWith(
expect.objectContaining({type: StateMachineEventType.KEYBOARD_EVENT_START}),
);
});
});
describe('onUserOpenEmoji', () => {
it('should dispatch USER_OPEN_EMOJI', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserOpenEmoji();
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.USER_OPEN_EMOJI,
});
});
});
describe('onUserCloseEmoji', () => {
it('should dispatch USER_CLOSE_EMOJI', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserCloseEmoji();
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.USER_CLOSE_EMOJI,
});
});
});
describe('onUserFocusEmojiSearch', () => {
it('should dispatch USER_FOCUS_EMOJI_SEARCH without hardware keyboard events by default', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserFocusEmojiSearch();
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.USER_FOCUS_EMOJI_SEARCH,
});
expect(ctx.processEvent).toHaveBeenCalledTimes(1);
});
it('should also dispatch KEYBOARD_EVENT_START and KEYBOARD_EVENT_END when asHardwareKeyboard=true', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserFocusEmojiSearch(true);
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.USER_FOCUS_EMOJI_SEARCH,
});
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.KEYBOARD_EVENT_START,
rawHeight: 0,
height: 0,
progress: 0,
});
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.KEYBOARD_EVENT_END,
rawHeight: 0,
height: 0,
progress: 0,
});
expect(ctx.processEvent).toHaveBeenCalledTimes(3);
});
});
describe('onUserBlurEmojiSearch', () => {
it('should dispatch USER_BLUR_EMOJI_SEARCH', async () => {
const ctx = makeContext();
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
result.current.onUserBlurEmojiSearch();
await flushScheduled();
expect(ctx.processEvent).toHaveBeenCalledWith({
type: StateMachineEventType.USER_BLUR_EMOJI_SEARCH,
});
});
});
describe('isEmojiPickerActive', () => {
it('should delegate to context.isEmojiPickerActive', () => {
const ctx = makeContext();
ctx.isEmojiPickerActive.mockReturnValue(true);
const {result} = renderHook(() => useKeyboardStateMachine(ctx as never));
expect(result.current.isEmojiPickerActive()).toBe(true);
expect(ctx.isEmojiPickerActive).toHaveBeenCalledTimes(1);
});
});
});