feat(MM-65625): enhance navigation overlay management (#9165)
* feat: enhance navigation overlay management - Add overlay tracking to NavigationStore with exception handling - Implement dismissAllOverlays with individual overlay dismissal - Add overlay management methods (add, remove, filter exceptions) - Preserve floating-banner-overlay when dismissing other overlays - Add comprehensive tests for overlay stack management - Update navigation command listeners for overlay events This improves overlay management by tracking overlays in the store and providing fine-grained control over which overlays to dismiss, particularly preserving floating banners during navigation cleanup. * make tests more meaningful in navigation * ChatGPT review fix * revert changes to dismissAllOverlays * create a new function dismissAllOverlaysWithExceptions * add new function removeAllOverlaysFromStack to clear the entire overlays * added a mock to dismissAllOverlays * update thread to call dismissAllOverlays with exceptions instead. * remove unneeded removeAllOverlaysWithExceptions() * silly ai * use set instead of array.
This commit is contained in:
parent
f6e8ebc2e9
commit
f1554f0341
6 changed files with 527 additions and 22 deletions
|
|
@ -13,7 +13,7 @@ import {getCurrentTeamId, getCurrentUserId, prepareCommonSystemValues, type Prep
|
|||
import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team';
|
||||
import {getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissAllModals, dismissAllModalsAndPopToRoot, dismissAllOverlays, goToScreen} from '@screens/navigation';
|
||||
import {dismissAllModals, dismissAllModalsAndPopToRoot, dismissAllOverlaysWithExceptions, goToScreen} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
|
@ -95,7 +95,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
|
|||
if (isFromNotification) {
|
||||
if (currentThreadId && currentThreadId === rootId && NavigationStore.getScreensInStack().includes(Screens.THREAD)) {
|
||||
await dismissAllModals();
|
||||
await dismissAllOverlays();
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,16 @@ import {Navigation} from 'react-native-navigation';
|
|||
import {Events, Preferences, Screens} from '@constants';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
|
||||
import {openAsBottomSheet} from './navigation';
|
||||
jest.unmock('@screens/navigation');
|
||||
|
||||
import * as navigationModule from './navigation';
|
||||
import {dismissAllOverlaysWithExceptions} from './navigation';
|
||||
|
||||
import type {FirstArgument} from '@typings/utils/utils';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
const {registerNavigationListeners} = navigationModule;
|
||||
|
||||
function expectShowModalCalledWith(screen: string, title: string, props?: Record<string, unknown>) {
|
||||
expect(Navigation.showModal).toHaveBeenCalledWith({
|
||||
stack: {
|
||||
|
|
@ -33,7 +38,7 @@ function expectShowModalOverCurrentContext(screen: string, props?: Record<string
|
|||
expectShowModalCalledWith(screen, '', props);
|
||||
}
|
||||
|
||||
function expectOpenAsBottomSheetCalledWith(props: FirstArgument<typeof openAsBottomSheet>, isTablet: boolean) {
|
||||
function expectOpenAsBottomSheetCalledWith(props: FirstArgument<typeof navigationModule.openAsBottomSheet>, isTablet: boolean) {
|
||||
if (isTablet) {
|
||||
expectShowModalCalledWith(props.screen, props.title, {closeButtonId: props.closeButtonId, ...props.props});
|
||||
} else {
|
||||
|
|
@ -107,3 +112,373 @@ describe('openUserProfileModal', () => {
|
|||
}, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overlay command listeners', () => {
|
||||
let mockCommandListener: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
NavigationStore.reset();
|
||||
|
||||
mockCommandListener = jest.fn();
|
||||
const mockEvents = {
|
||||
registerScreenPoppedListener: jest.fn(() => ({remove: jest.fn()})),
|
||||
registerCommandListener: jest.fn((listener) => {
|
||||
mockCommandListener = listener;
|
||||
return {remove: jest.fn()};
|
||||
}),
|
||||
registerComponentWillAppearListener: jest.fn(() => ({remove: jest.fn()})),
|
||||
};
|
||||
(Navigation.events as jest.Mock).mockReturnValue(mockEvents);
|
||||
|
||||
registerNavigationListeners();
|
||||
});
|
||||
|
||||
it('should call NavigationStore.addOverlayToStack when showOverlay command is received', () => {
|
||||
const overlayId = 'test-overlay-id';
|
||||
const params = {layout: {id: overlayId}};
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([]);
|
||||
|
||||
mockCommandListener('showOverlay', params);
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([overlayId]);
|
||||
});
|
||||
|
||||
it('should call NavigationStore.removeOverlayFromStack when dismissOverlay command is received', () => {
|
||||
const overlayId = 'test-overlay-id';
|
||||
NavigationStore.addOverlayToStack(overlayId);
|
||||
NavigationStore.addOverlayToStack('another-overlay');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['another-overlay', overlayId]);
|
||||
|
||||
mockCommandListener('dismissOverlay', {componentId: overlayId});
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['another-overlay']);
|
||||
});
|
||||
|
||||
it('should call NavigationStore.removeAllOverlaysFromStack when dismissAllOverlays command is received', () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['overlay2', 'floating-banner-overlay', 'overlay1']);
|
||||
|
||||
mockCommandListener('dismissAllOverlays', {});
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not affect NavigationStore when unrecognized commands are received', () => {
|
||||
NavigationStore.addOverlayToStack('test-overlay');
|
||||
const initialOverlays = NavigationStore.getOverlaysInStack();
|
||||
|
||||
mockCommandListener('someOtherCommand', {});
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(initialOverlays);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showOverlay', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
NavigationStore.reset();
|
||||
});
|
||||
|
||||
it('should call Navigation.showOverlay with correct parameters', () => {
|
||||
const screenName = Screens.USER_PROFILE;
|
||||
const passProps = {userId: 'user123'};
|
||||
const options = {overlay: {interceptTouchOutside: true}};
|
||||
const id = 'custom-overlay-id';
|
||||
|
||||
navigationModule.showOverlay(screenName, passProps, options, id);
|
||||
|
||||
expect(Navigation.showOverlay).toHaveBeenCalledWith({
|
||||
component: {
|
||||
id: 'custom-overlay-id',
|
||||
name: screenName,
|
||||
passProps,
|
||||
options: expect.objectContaining({
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
componentBackgroundColor: 'transparent',
|
||||
},
|
||||
overlay: {
|
||||
interceptTouchOutside: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default options when none provided', () => {
|
||||
const screenName = Screens.USER_PROFILE;
|
||||
|
||||
navigationModule.showOverlay(screenName);
|
||||
|
||||
expect(Navigation.showOverlay).toHaveBeenCalledWith({
|
||||
component: {
|
||||
id: undefined,
|
||||
name: screenName,
|
||||
passProps: {},
|
||||
options: {
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
componentBackgroundColor: 'transparent',
|
||||
},
|
||||
overlay: {
|
||||
interceptTouchOutside: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge custom options with default options correctly', () => {
|
||||
const screenName = Screens.USER_PROFILE;
|
||||
const customOptions = {
|
||||
layout: {backgroundColor: 'red'},
|
||||
overlay: {interceptTouchOutside: true},
|
||||
customProperty: 'test',
|
||||
};
|
||||
|
||||
navigationModule.showOverlay(screenName, {}, customOptions);
|
||||
|
||||
expect(Navigation.showOverlay).toHaveBeenCalledWith({
|
||||
component: {
|
||||
id: undefined,
|
||||
name: screenName,
|
||||
passProps: {},
|
||||
options: expect.objectContaining({
|
||||
layout: {
|
||||
backgroundColor: 'red',
|
||||
componentBackgroundColor: 'transparent',
|
||||
},
|
||||
overlay: {
|
||||
interceptTouchOutside: true,
|
||||
},
|
||||
customProperty: 'test',
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissOverlay', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
NavigationStore.reset();
|
||||
});
|
||||
|
||||
it('should call Navigation.dismissOverlay with correct componentId', async () => {
|
||||
const componentId = 'overlay-123';
|
||||
|
||||
await navigationModule.dismissOverlay(componentId);
|
||||
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
|
||||
it('should handle Navigation.dismissOverlay rejection gracefully', async () => {
|
||||
const componentId = 'non-existent-overlay';
|
||||
|
||||
(Navigation.dismissOverlay as jest.Mock).mockRejectedValueOnce(new Error('Overlay not found'));
|
||||
|
||||
await expect(navigationModule.dismissOverlay(componentId)).resolves.not.toThrow();
|
||||
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissAllOverlays', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
NavigationStore.reset();
|
||||
});
|
||||
|
||||
it('should call Navigation.dismissAllOverlays', async () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['overlay2', 'floating-banner-overlay', 'overlay1']);
|
||||
|
||||
await navigationModule.dismissAllOverlays();
|
||||
|
||||
expect(Navigation.dismissAllOverlays).toHaveBeenCalledTimes(1);
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty overlay stack gracefully', async () => {
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([]);
|
||||
|
||||
await navigationModule.dismissAllOverlays();
|
||||
|
||||
expect(Navigation.dismissAllOverlays).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle only exception overlays in stack', async () => {
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['floating-banner-overlay']);
|
||||
|
||||
await navigationModule.dismissAllOverlays();
|
||||
|
||||
expect(Navigation.dismissAllOverlays).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle Navigation.dismissAllOverlays rejection gracefully', async () => {
|
||||
(Navigation.dismissAllOverlays as jest.Mock).mockRejectedValueOnce(new Error('Dismiss failed'));
|
||||
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
await expect(navigationModule.dismissAllOverlays()).resolves.not.toThrow();
|
||||
|
||||
expect(Navigation.dismissAllOverlays).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call Navigation.dismissAllOverlays once regardless of overlay count', async () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
NavigationStore.addOverlayToStack('overlay3');
|
||||
|
||||
await navigationModule.dismissAllOverlays();
|
||||
|
||||
expect(Navigation.dismissAllOverlays).toHaveBeenCalledTimes(1);
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissAllModalsAndPopToRoot', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
NavigationStore.reset();
|
||||
});
|
||||
|
||||
it('should call dismissAllModals, dismissAllOverlaysWithExceptions, and popToRoot in sequence', async () => {
|
||||
NavigationStore.addModalToStack('modal1');
|
||||
NavigationStore.addScreenToStack('screen1');
|
||||
|
||||
await navigationModule.dismissAllModalsAndPopToRoot();
|
||||
|
||||
expect(Navigation.dismissModal).toHaveBeenCalledTimes(1);
|
||||
expect(Navigation.dismissModal).toHaveBeenCalledWith('modal1', {animations: {dismissModal: {enabled: false}}});
|
||||
expect(Navigation.popToRoot).toHaveBeenCalledTimes(1);
|
||||
expect(Navigation.popToRoot).toHaveBeenCalledWith('screen1');
|
||||
});
|
||||
|
||||
it('should dismiss non-exception overlays via dismissAllOverlaysWithExceptions', async () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['overlay2', 'floating-banner-overlay', 'overlay1']);
|
||||
|
||||
await navigationModule.dismissAllModalsAndPopToRoot();
|
||||
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledTimes(2);
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay2');
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay1');
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalledWith('floating-banner-overlay');
|
||||
});
|
||||
|
||||
it('should handle empty overlay stack gracefully', async () => {
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([]);
|
||||
NavigationStore.addScreenToStack('screen1');
|
||||
|
||||
await navigationModule.dismissAllModalsAndPopToRoot();
|
||||
|
||||
expect(Navigation.dismissModal).not.toHaveBeenCalled();
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalled();
|
||||
expect(Navigation.popToRoot).toHaveBeenCalledTimes(1);
|
||||
expect(Navigation.popToRoot).toHaveBeenCalledWith('screen1');
|
||||
});
|
||||
|
||||
it('should continue with popToRoot even if overlay dismissal fails', async () => {
|
||||
(Navigation.dismissOverlay as jest.Mock).mockRejectedValueOnce(new Error('Dismiss failed'));
|
||||
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addScreenToStack('screen1');
|
||||
|
||||
await expect(navigationModule.dismissAllModalsAndPopToRoot()).resolves.not.toThrow();
|
||||
|
||||
expect(Navigation.dismissModal).not.toHaveBeenCalled();
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay1');
|
||||
expect(Navigation.popToRoot).toHaveBeenCalledTimes(1);
|
||||
expect(Navigation.popToRoot).toHaveBeenCalledWith('screen1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissAllOverlaysWithExceptions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
NavigationStore.reset();
|
||||
});
|
||||
|
||||
it('should dismiss non-exception overlays individually', async () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['overlay2', 'floating-banner-overlay', 'overlay1']);
|
||||
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledTimes(2);
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay2');
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay1');
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalledWith('floating-banner-overlay');
|
||||
});
|
||||
|
||||
it('should handle empty overlay stack gracefully', async () => {
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([]);
|
||||
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle only exception overlays in stack', async () => {
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['floating-banner-overlay']);
|
||||
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
|
||||
expect(Navigation.dismissOverlay).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use Promise.all for concurrent dismissals', async () => {
|
||||
const dismissalOrder: string[] = [];
|
||||
(Navigation.dismissOverlay as jest.Mock).mockImplementation((overlayId: string) => {
|
||||
dismissalOrder.push(overlayId);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
NavigationStore.addOverlayToStack('overlay3');
|
||||
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledTimes(3);
|
||||
expect(dismissalOrder).toContain('overlay1');
|
||||
expect(dismissalOrder).toContain('overlay2');
|
||||
expect(dismissalOrder).toContain('overlay3');
|
||||
});
|
||||
|
||||
it('should continue dismissing even if one overlay dismissal fails', async () => {
|
||||
(Navigation.dismissOverlay as jest.Mock).
|
||||
mockImplementationOnce(() => Promise.reject(new Error('Dismiss failed'))).
|
||||
mockImplementationOnce(() => Promise.resolve());
|
||||
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
await expect(dismissAllOverlaysWithExceptions()).resolves.not.toThrow();
|
||||
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledTimes(2);
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay2');
|
||||
expect(Navigation.dismissOverlay).toHaveBeenCalledWith('overlay1');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -76,6 +76,16 @@ function onCommandListener(name: string, params: any) {
|
|||
case 'dismissModal':
|
||||
NavigationStore.removeModalFromStack(params.componentId);
|
||||
break;
|
||||
case 'showOverlay':
|
||||
NavigationStore.addOverlayToStack(params?.layout?.id);
|
||||
break;
|
||||
case 'dismissOverlay':
|
||||
NavigationStore.removeOverlayFromStack(params?.componentId);
|
||||
break;
|
||||
case 'dismissAllOverlays': {
|
||||
NavigationStore.removeAllOverlaysFromStack();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (NavigationStore.getVisibleScreen() === Screens.HOME) {
|
||||
|
|
@ -553,7 +563,7 @@ export async function popToRoot() {
|
|||
|
||||
export async function dismissAllModalsAndPopToRoot() {
|
||||
await dismissAllModals();
|
||||
await dismissAllOverlays();
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
await popToRoot();
|
||||
}
|
||||
|
||||
|
|
@ -567,7 +577,7 @@ export async function dismissAllModalsAndPopToRoot() {
|
|||
*/
|
||||
export async function dismissAllModalsAndPopToScreen(screenId: AvailableScreens, title: string, passProps = {}, options = {}) {
|
||||
await dismissAllModals();
|
||||
await dismissAllOverlays();
|
||||
await dismissAllOverlaysWithExceptions();
|
||||
if (NavigationStore.getScreensInStack().includes(screenId)) {
|
||||
let mergeOptions = options;
|
||||
if (title) {
|
||||
|
|
@ -791,6 +801,28 @@ export async function dismissOverlay(componentId: string) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of using native dismissAllOverlays, we're looping through the overlays
|
||||
* and dismissing them individually. Native dismissAllOverlays is causing the app to
|
||||
* dismiss 700+ overlays. Even though those overlays doesn't exist in the stack. Since we're
|
||||
* tracking the overlays in the store, we can dismiss them individually.
|
||||
* @returns
|
||||
*/
|
||||
export async function dismissAllOverlaysWithExceptions() {
|
||||
try {
|
||||
const overlaysToRemove = NavigationStore.getAllOverlaysOtherThanExceptions();
|
||||
if (!overlaysToRemove.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(overlaysToRemove.map((overlayId) =>
|
||||
Navigation.dismissOverlay(overlayId).catch(() => undefined),
|
||||
));
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAllOverlays() {
|
||||
try {
|
||||
await Navigation.dismissAllOverlays();
|
||||
|
|
@ -912,3 +944,4 @@ export async function openUserProfileModal(
|
|||
Keyboard.dismiss();
|
||||
openAsBottomSheet({screen, title, theme, closeButtonId, props: {...props}});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,77 @@ describe('NavigationStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('overlay management', () => {
|
||||
it('should add and remove overlays correctly', () => {
|
||||
NavigationStore.addOverlayToStack('test-overlay');
|
||||
NavigationStore.addOverlayToStack('another-overlay');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['another-overlay', 'test-overlay']);
|
||||
});
|
||||
|
||||
it('should handle duplicate overlay additions', () => {
|
||||
NavigationStore.addOverlayToStack('test-overlay');
|
||||
NavigationStore.addOverlayToStack('test-overlay');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['test-overlay']);
|
||||
});
|
||||
|
||||
it('should remove specific overlays from stack', () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
NavigationStore.addOverlayToStack('overlay3');
|
||||
|
||||
NavigationStore.removeOverlayFromStack('overlay2');
|
||||
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual(['overlay3', 'overlay1']);
|
||||
});
|
||||
|
||||
it('should return overlays other than exceptions', () => {
|
||||
NavigationStore.addOverlayToStack('regular-overlay');
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
NavigationStore.addOverlayToStack('another-overlay');
|
||||
|
||||
const overlaysToRemove = NavigationStore.getAllOverlaysOtherThanExceptions();
|
||||
|
||||
expect(overlaysToRemove).toEqual(['another-overlay', 'regular-overlay']);
|
||||
expect(overlaysToRemove).not.toContain('floating-banner-overlay');
|
||||
});
|
||||
|
||||
it('should return empty array when only exception overlays exist', () => {
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
|
||||
const overlaysToRemove = NavigationStore.getAllOverlaysOtherThanExceptions();
|
||||
|
||||
expect(overlaysToRemove).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all overlays when no exceptions exist in stack', () => {
|
||||
NavigationStore.addOverlayToStack('overlay1');
|
||||
NavigationStore.addOverlayToStack('overlay2');
|
||||
|
||||
const overlaysToRemove = NavigationStore.getAllOverlaysOtherThanExceptions();
|
||||
|
||||
expect(overlaysToRemove).toEqual(['overlay2', 'overlay1']);
|
||||
});
|
||||
|
||||
it('should handle empty overlay stack gracefully when getting overlays to remove', () => {
|
||||
const overlaysToRemove = NavigationStore.getAllOverlaysOtherThanExceptions();
|
||||
expect(overlaysToRemove).toEqual([]);
|
||||
expect(NavigationStore.getOverlaysInStack()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should maintain overlay order when filtering', () => {
|
||||
NavigationStore.addOverlayToStack('first-overlay');
|
||||
NavigationStore.addOverlayToStack('floating-banner-overlay');
|
||||
NavigationStore.addOverlayToStack('second-overlay');
|
||||
NavigationStore.addOverlayToStack('third-overlay');
|
||||
|
||||
const overlaysToRemove = NavigationStore.getAllOverlaysOtherThanExceptions();
|
||||
|
||||
expect(overlaysToRemove).toEqual(['third-overlay', 'second-overlay', 'first-overlay']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('screen loading and visibility', () => {
|
||||
it('should handle waitUntilScreenHasLoaded', async () => {
|
||||
const promise = NavigationStore.waitUntilScreenHasLoaded(Screens.ABOUT);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import {BehaviorSubject} from 'rxjs';
|
|||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const OVERLAY_EXCEPTIONS = new Set(['floating-banner-overlay']);
|
||||
|
||||
class NavigationStoreSingleton {
|
||||
private screensInStack: AvailableScreens[] = [];
|
||||
private modalsInStack: AvailableScreens[] = [];
|
||||
private overlaysInStack: string[] = [];
|
||||
private visibleTab = 'Home';
|
||||
private tosOpen = false;
|
||||
|
||||
|
|
@ -20,6 +23,7 @@ class NavigationStoreSingleton {
|
|||
reset = () => {
|
||||
this.screensInStack = [];
|
||||
this.modalsInStack = [];
|
||||
this.overlaysInStack = [];
|
||||
this.visibleTab = 'Home';
|
||||
this.tosOpen = false;
|
||||
this.subject.next(undefined);
|
||||
|
|
@ -85,6 +89,28 @@ class NavigationStoreSingleton {
|
|||
}
|
||||
};
|
||||
|
||||
addOverlayToStack = (overlayId: string) => {
|
||||
this.removeOverlayFromStack(overlayId);
|
||||
this.overlaysInStack.unshift(overlayId);
|
||||
};
|
||||
|
||||
removeOverlayFromStack = (overlayId: string) => {
|
||||
const index = this.overlaysInStack.indexOf(overlayId);
|
||||
if (index > -1) {
|
||||
this.overlaysInStack.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
getOverlaysInStack = () => this.overlaysInStack;
|
||||
|
||||
removeAllOverlaysFromStack = () => {
|
||||
this.overlaysInStack = [];
|
||||
};
|
||||
|
||||
getAllOverlaysOtherThanExceptions = () => {
|
||||
return this.overlaysInStack.filter((overlayId) => !OVERLAY_EXCEPTIONS.has(overlayId));
|
||||
};
|
||||
|
||||
setToSOpen = (open: boolean) => {
|
||||
this.tosOpen = open;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
|
||||
import {setGenerator} from '@nozbe/watermelondb/utils/common/randomId';
|
||||
import * as ReactNative from 'react-native';
|
||||
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
|
||||
|
|
@ -342,22 +340,23 @@ jest.mock('react-native-navigation', () => {
|
|||
RNN.Navigation.setLazyComponentRegistrator = jest.fn();
|
||||
RNN.Navigation.setDefaultOptions = jest.fn();
|
||||
RNN.Navigation.registerComponent = jest.fn();
|
||||
|
||||
const mockSubscription = {remove: jest.fn()};
|
||||
const createMockEvents = () => ({
|
||||
registerAppLaunchedListener: jest.fn(() => mockSubscription),
|
||||
registerComponentListener: jest.fn(() => mockSubscription),
|
||||
bindComponent: jest.fn(() => mockSubscription),
|
||||
registerNavigationButtonPressedListener: jest.fn(() => mockSubscription),
|
||||
registerScreenPoppedListener: jest.fn(() => mockSubscription),
|
||||
registerCommandListener: jest.fn(() => mockSubscription),
|
||||
registerComponentWillAppearListener: jest.fn(() => mockSubscription),
|
||||
});
|
||||
|
||||
return {
|
||||
...RNN,
|
||||
Navigation: {
|
||||
...RNN.Navigation,
|
||||
events: () => ({
|
||||
registerAppLaunchedListener: jest.fn(),
|
||||
registerComponentListener: jest.fn(() => {
|
||||
return {remove: jest.fn()};
|
||||
}),
|
||||
bindComponent: jest.fn(() => {
|
||||
return {remove: jest.fn()};
|
||||
}),
|
||||
registerNavigationButtonPressedListener: jest.fn(() => {
|
||||
return {remove: jest.fn()};
|
||||
}),
|
||||
}),
|
||||
events: jest.fn(createMockEvents),
|
||||
setRoot: jest.fn(),
|
||||
pop: jest.fn(),
|
||||
push: jest.fn(),
|
||||
|
|
@ -367,7 +366,8 @@ jest.mock('react-native-navigation', () => {
|
|||
popToRoot: jest.fn(),
|
||||
mergeOptions: jest.fn(),
|
||||
showOverlay: jest.fn(),
|
||||
dismissOverlay: jest.fn(),
|
||||
dismissOverlay: jest.fn(() => Promise.resolve()),
|
||||
dismissAllOverlays: jest.fn(() => Promise.resolve()),
|
||||
updateProps: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
|
@ -395,7 +395,7 @@ jest.mock('react-native-notifications', () => {
|
|||
ios: {
|
||||
getDeliveredNotifications: jest.fn().mockImplementation(() => Promise.resolve(deliveredNotifications)),
|
||||
removeDeliveredNotifications: jest.fn((ids) => {
|
||||
// eslint-disable-next-line
|
||||
|
||||
// @ts-ignore
|
||||
deliveredNotifications = deliveredNotifications.filter((n) => !ids.includes(n.identifier));
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in a new issue