Skip to content

Commit

Permalink
✨ to handle multiple prevent exit
Browse files Browse the repository at this point in the history
  • Loading branch information
arunachalam-monk committed Jun 27, 2024
1 parent 7d9655a commit 70171e6
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 52 deletions.
2 changes: 1 addition & 1 deletion configs/test-utils/src/__mocks__/@monkvision/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,5 @@ export = {
useAsyncInterval: jest.fn(),
useSearchParams: jest.fn(() => ({ get: jest.fn(() => null) })),
useObjectMemo: jest.fn((obj) => obj),
usePreventExit: jest.fn(),
usePreventExit: jest.fn(() => ({ allowRedirect: jest.fn() })),
};
25 changes: 25 additions & 0 deletions packages/common/src/PreventExit/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useMemo } from 'react';
import { createPreventExitListener } from './store';

/**
* Custom hook that allows you
* to access the PreventExit Context methods inside a component.
*
* Note : If this hook is called inside a component
* that is not a child of a PreventExit component,
* it will throw an error.
*
* @example
* ```tsx
* function MyComponent() {
* const { forceOut } = usePreventExit(true);// commonly it should be a expression.
* return <div onClick={() => forceOut()}>My Component</div>;
* }
* ```
*/
export function usePreventExit(preventExit: boolean) {
const { cleanup, setPreventExit, allowRedirect } = useMemo(createPreventExitListener, []);
useMemo(() => setPreventExit(preventExit), [preventExit]);
useEffect(() => cleanup, []);
return { allowRedirect };
}
1 change: 1 addition & 0 deletions packages/common/src/PreventExit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './hooks';
49 changes: 49 additions & 0 deletions packages/common/src/PreventExit/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const keys: Array<symbol> = [];
const allPreventExitState: Record<symbol, boolean> = {};

function checkNoMorePreventExit() {
if (keys.map((key) => allPreventExitState[key]).every((i) => i === false)) {
window.onbeforeunload = null;
return true;
}
return false;
}
function publish(id: symbol, preventExit: boolean) {
allPreventExitState[id] = preventExit;
if (!checkNoMorePreventExit())
window.onbeforeunload = (e) => {
e.preventDefault();
return 'Confirm Alert appears';
};
}
/**
* Creates a listener function that manages the preventExit state of a component.
*/
export function createPreventExitListener() {
const key = Symbol('PreventExitListener');
allPreventExitState[key] = true;
keys.push(key);
return {
/**
* To change the preventExit state of the component.
*/
setPreventExit: (preventExit: boolean) => {
publish(key, preventExit);
},
/**
* Allows the user to leave the page without confirmation temporarily.
* This should be used when the developer wants to explicitly allow navigation.
*/
allowRedirect: () => {
window.onbeforeunload = null;
},
/**
* Performs garbage collection by removing the preventExit state associated with the component.
* This should be used when the component is unmounted.
*/
cleanup: () => {
delete allPreventExitState[key];
checkNoMorePreventExit();
},
};
}
1 change: 0 additions & 1 deletion packages/common/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@ export * from './useSearchParams';
export * from './useInterval';
export * from './useAsyncInterval';
export * from './useObjectMemo';
export * from './usePreventExit';
25 changes: 0 additions & 25 deletions packages/common/src/hooks/usePreventExit.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './PreventExit';
export * from './apps';
export * from './hooks';
export * from './i18n';
export * from './state';
export * from './theme';
export * from './utils';
export * from './hooks';
export * from './apps';
46 changes: 46 additions & 0 deletions packages/common/test/PreventExit/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { renderHook } from '@testing-library/react-hooks';
import { usePreventExit } from '../../src/PreventExit/hooks';
import { createPreventExitListener } from '../../src/PreventExit/store';

jest.mock('../../src/PreventExit/store', () => ({
createPreventExitListener: jest.fn(() => ({
cleanup: jest.fn(),
setPreventExit: jest.fn(),
allowRedirect: jest.fn(),
})),
}));
describe('PreventExit hook usePreventExit', () => {
let spyCreatePreventExitListener: jest.SpyInstance<typeof createPreventExitListener>;
beforeEach(() => {
spyCreatePreventExitListener = jest.spyOn(
require('../../src/PreventExit/store'),
'createPreventExitListener',
);
});
afterEach(() => jest.clearAllMocks());

it('should clean up when unmount', () => {
const { unmount } = renderHook(() => usePreventExit(true));
unmount();
expect(spyCreatePreventExitListener.mock.results.at(-1)?.value.cleanup).toHaveBeenCalledTimes(
1,
);
});

it('should set preventExit when value changes', async () => {
const { rerender } = renderHook((props) => usePreventExit(props), {
initialProps: true,
});
expect(
spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
).toHaveBeenCalledTimes(1);
rerender(false);
expect(
spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
).toHaveBeenCalledTimes(2);
rerender(false);
expect(
spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
).toHaveBeenCalledTimes(2);
});
});
49 changes: 49 additions & 0 deletions packages/common/test/PreventExit/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createPreventExitListener } from '../../src/PreventExit/store';

describe('preventExitStore', () => {
let listener1: ReturnType<typeof createPreventExitListener>;
let listener2: ReturnType<typeof createPreventExitListener>;

beforeEach(() => {
listener1 = createPreventExitListener();
listener2 = createPreventExitListener();
});

afterEach(() => {
listener1.cleanup();
listener2.cleanup();
});

it('should prevent exit: listener 1', () => {
listener1.setPreventExit(true);
listener2.setPreventExit(true);
expect(window.onbeforeunload).not.toBe(null);
listener1.setPreventExit(true);
listener2.setPreventExit(false);
expect(window.onbeforeunload).not.toBe(null);
listener1.setPreventExit(false);
listener2.setPreventExit(true);
expect(window.onbeforeunload).not.toBe(null);
listener1.setPreventExit(false);
listener2.setPreventExit(false);
expect(window.onbeforeunload).toBe(null);
});

it('should allow redirect: listener 1', () => {
const preventExit = [true, false];
preventExit.forEach((i) => {
preventExit.forEach((j) => {
listener1.setPreventExit(i);
listener2.setPreventExit(j);
listener1.allowRedirect();
expect(window.onbeforeunload).toBe(null);
});
});
});

it('should allow redirect: lister 2', () => {
listener2.cleanup();
listener1.cleanup();
expect(window.onbeforeunload).toBe(null);
});
});
19 changes: 0 additions & 19 deletions packages/common/test/hooks/usePreventExit.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useI18nSync,
useLoadingState,
useObjectMemo,
usePreventExit,
useWindowDimensions,
} from '@monkvision/common';
import {
Expand Down Expand Up @@ -212,6 +213,7 @@ export function PhotoCapture({
}
setCurrentScreen(PhotoCaptureScreen.CAMERA);
};
const { allowRedirect } = usePreventExit(sightState.sightsTaken.length !== 0);
const handleGalleryValidate = () => {
startTasks()
.then(() => {
Expand All @@ -220,6 +222,7 @@ export function PhotoCapture({
captureCompleted: true,
sightSelected: 'inspection-completed',
});
allowRedirect();
onComplete?.();
})
.catch((err) => {
Expand All @@ -230,7 +233,6 @@ export function PhotoCapture({
const isViolatingEnforcedOrientation =
enforceOrientation &&
(enforceOrientation === DeviceOrientation.PORTRAIT) !== dimensions.isPortrait;

const hudProps: Omit<PhotoCaptureHUDProps, keyof CameraHUDProps> = {
sights,
selectedSight: sightState.selectedSight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ jest.mock('../../src/PhotoCapture/hooks', () => ({
useTracking: jest.fn(),
}));

import { act, render, waitFor } from '@testing-library/react';
import { Camera } from '@monkvision/camera-web';
import { expectPropsOnChildMock } from '@monkvision/test-utils';
import { useI18nSync, useLoadingState } from '@monkvision/common';
import { useI18nSync, useLoadingState, usePreventExit } from '@monkvision/common';
import { BackdropDialog, InspectionGallery } from '@monkvision/common-ui-web';
import { useMonitoring } from '@monkvision/monitoring';
import { expectPropsOnChildMock } from '@monkvision/test-utils';
import { act, render, waitFor } from '@testing-library/react';
import { PhotoCapture, PhotoCaptureHUD, PhotoCaptureProps } from '../../src';
import {
useAdaptiveCameraConfig,
Expand Down Expand Up @@ -434,4 +434,11 @@ describe('PhotoCapture component', () => {

unmount();
});

it('should ask the user for confirmation before exit', () => {
const props = createProps();
const { unmount } = render(<PhotoCapture {...props} />);
expect(usePreventExit).toHaveBeenCalled();
unmount();
});
});

0 comments on commit 70171e6

Please sign in to comment.