Skip to content

Commit ae5e3d5

Browse files
Merge pull request #43 from alichherawalla/bugfix/OGM-38
bugfix: issue #38 and #17
2 parents 5bc4a55 + 507336c commit ae5e3d5

4 files changed

Lines changed: 512 additions & 5 deletions

File tree

__tests__/rntl/components/AppSheet.test.tsx

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
*/
1717

1818
import React from 'react';
19-
import { Text } from 'react-native';
20-
import { render, fireEvent, waitFor } from '@testing-library/react-native';
19+
import { Text, Keyboard } from 'react-native';
20+
import { render, fireEvent, waitFor, act } from '@testing-library/react-native';
2121
import { AppSheet } from '../../../src/components/AppSheet';
2222

2323
describe('AppSheet', () => {
@@ -275,6 +275,193 @@ describe('AppSheet', () => {
275275
});
276276
});
277277

278+
// ============================================================================
279+
// Keyboard Dismiss Before Open
280+
// ============================================================================
281+
describe('keyboard dismiss before open', () => {
282+
let mockRemove: jest.Mock;
283+
let mockAddListener: jest.SpyInstance;
284+
let mockDismiss: jest.SpyInstance;
285+
let mockIsVisible: jest.SpyInstance;
286+
287+
beforeEach(() => {
288+
mockRemove = jest.fn();
289+
mockAddListener = jest.spyOn(Keyboard, 'addListener').mockReturnValue({
290+
remove: mockRemove,
291+
} as any);
292+
mockDismiss = jest.spyOn(Keyboard, 'dismiss').mockImplementation(() => { });
293+
mockIsVisible = jest.spyOn(Keyboard, 'isVisible' as any);
294+
});
295+
296+
afterEach(() => {
297+
mockAddListener.mockRestore();
298+
mockDismiss.mockRestore();
299+
mockIsVisible.mockRestore();
300+
});
301+
302+
it('opens modal immediately when keyboard is not visible', () => {
303+
mockIsVisible.mockReturnValue(false);
304+
305+
const { toJSON } = render(
306+
<AppSheet visible={true} onClose={jest.fn()} title="Sheet">
307+
<Text>Content</Text>
308+
</AppSheet>
309+
);
310+
311+
expect(Keyboard.dismiss).not.toHaveBeenCalled();
312+
// addListener may be called by KeyboardAvoidingView internally,
313+
// but should NOT be called with 'keyboardDidHide' by our code
314+
const didHideCalls = mockAddListener.mock.calls.filter(
315+
(call: any[]) => call[0] === 'keyboardDidHide',
316+
);
317+
expect(didHideCalls).toHaveLength(0);
318+
expect(toJSON()).toBeTruthy();
319+
});
320+
321+
it('dismisses keyboard and defers modal when keyboard is visible', () => {
322+
mockIsVisible.mockReturnValue(true);
323+
324+
const { toJSON } = render(
325+
<AppSheet visible={false} onClose={jest.fn()} title="Sheet">
326+
<Text>Content</Text>
327+
</AppSheet>
328+
);
329+
330+
// Initially not visible
331+
expect(toJSON()).toBeNull();
332+
333+
// Now set visible — keyboard is open
334+
render(
335+
<AppSheet visible={true} onClose={jest.fn()} title="Sheet">
336+
<Text>Content</Text>
337+
</AppSheet>
338+
);
339+
340+
expect(Keyboard.dismiss).toHaveBeenCalled();
341+
expect(Keyboard.addListener).toHaveBeenCalledWith(
342+
'keyboardDidHide',
343+
expect.any(Function),
344+
);
345+
});
346+
347+
it('opens modal after keyboardDidHide event fires', async () => {
348+
mockIsVisible.mockReturnValue(true);
349+
let keyboardHideCallback: (() => void) | null = null;
350+
mockAddListener.mockImplementation((_event: string, cb: () => void) => {
351+
keyboardHideCallback = cb;
352+
return { remove: mockRemove };
353+
});
354+
355+
const { rerender, getByText } = render(
356+
<AppSheet visible={false} onClose={jest.fn()} title="Sheet">
357+
<Text>Content</Text>
358+
</AppSheet>
359+
);
360+
361+
// Open the sheet — keyboard is visible, so modal deferred
362+
rerender(
363+
<AppSheet visible={true} onClose={jest.fn()} title="Sheet">
364+
<Text>Content</Text>
365+
</AppSheet>
366+
);
367+
368+
expect(Keyboard.dismiss).toHaveBeenCalled();
369+
370+
// Simulate keyboard finishing its dismiss
371+
await act(() => {
372+
keyboardHideCallback!();
373+
});
374+
375+
// Modal should now be visible with content
376+
expect(getByText('Sheet')).toBeTruthy();
377+
expect(mockRemove).toHaveBeenCalled();
378+
});
379+
380+
it('opens modal via safety timeout if keyboardDidHide never fires', async () => {
381+
jest.useFakeTimers();
382+
mockIsVisible.mockReturnValue(true);
383+
384+
const { rerender, getByText } = render(
385+
<AppSheet visible={false} onClose={jest.fn()} title="Sheet">
386+
<Text>Content</Text>
387+
</AppSheet>
388+
);
389+
390+
rerender(
391+
<AppSheet visible={true} onClose={jest.fn()} title="Sheet">
392+
<Text>Content</Text>
393+
</AppSheet>
394+
);
395+
396+
expect(Keyboard.dismiss).toHaveBeenCalled();
397+
398+
// Fast-forward past the 400ms safety timeout
399+
await act(() => {
400+
jest.advanceTimersByTime(400);
401+
});
402+
403+
expect(getByText('Sheet')).toBeTruthy();
404+
expect(mockRemove).toHaveBeenCalled();
405+
406+
jest.useRealTimers();
407+
});
408+
409+
it('does not open modal twice if both listener and timeout fire', async () => {
410+
jest.useFakeTimers();
411+
mockIsVisible.mockReturnValue(true);
412+
let keyboardHideCallback: (() => void) | null = null;
413+
mockAddListener.mockImplementation((_event: string, cb: () => void) => {
414+
keyboardHideCallback = cb;
415+
return { remove: mockRemove };
416+
});
417+
418+
const { rerender } = render(
419+
<AppSheet visible={false} onClose={jest.fn()} title="Sheet">
420+
<Text>Content</Text>
421+
</AppSheet>
422+
);
423+
424+
rerender(
425+
<AppSheet visible={true} onClose={jest.fn()} title="Sheet">
426+
<Text>Content</Text>
427+
</AppSheet>
428+
);
429+
430+
// Fire the keyboard hide callback
431+
await act(() => {
432+
keyboardHideCallback!();
433+
});
434+
435+
// Also fire the timeout — should be a no-op
436+
await act(() => {
437+
jest.advanceTimersByTime(400);
438+
});
439+
440+
// No errors — the guard prevents double setState
441+
jest.useRealTimers();
442+
});
443+
444+
it('cleans up listener and timeout on unmount during keyboard dismiss', () => {
445+
jest.useFakeTimers();
446+
mockIsVisible.mockReturnValue(true);
447+
448+
const { unmount } = render(
449+
<AppSheet visible={true} onClose={jest.fn()} title="Sheet">
450+
<Text>Content</Text>
451+
</AppSheet>
452+
);
453+
454+
expect(Keyboard.addListener).toHaveBeenCalled();
455+
456+
unmount();
457+
458+
// Cleanup should have removed the listener
459+
expect(mockRemove).toHaveBeenCalled();
460+
461+
jest.useRealTimers();
462+
});
463+
});
464+
278465
// ============================================================================
279466
// Visibility Transitions
280467
// ============================================================================

0 commit comments

Comments
 (0)