You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Chore: Mobile: Add additional plugin panel integration tests (#13152)
This commit is contained in:
@@ -924,6 +924,7 @@ packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/appReducer.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -897,6 +897,7 @@ packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/appReducer.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
packages/app-mobile/utils/checkPermissions.js
|
||||
|
||||
@@ -6,11 +6,12 @@ import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||
import PluginRunnerWebView from './PluginRunnerWebView';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import { render, waitFor } from '../../utils/testing/testingLibrary';
|
||||
import { act, render, screen, waitFor } from '../../utils/testing/testingLibrary';
|
||||
import createTestPlugin from '@joplin/lib/testing/plugins/createTestPlugin';
|
||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
@@ -30,6 +31,16 @@ const defaultManifestProperties = {
|
||||
name: 'Some plugin name',
|
||||
};
|
||||
|
||||
type PluginSlice = { manifest: { id: string } };
|
||||
const waitForPluginToLoad = (plugin: PluginSlice) => {
|
||||
return waitFor(async () => {
|
||||
expect(PluginService.instance().pluginById(plugin.manifest.id)).toBeTruthy();
|
||||
});
|
||||
};
|
||||
|
||||
const webViewId = 'joplin__PluginDialogWebView';
|
||||
const getUserWebViewDom = () => getWebViewDomById(webViewId);
|
||||
|
||||
describe('PluginRunnerWebView', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
@@ -56,16 +67,68 @@ describe('PluginRunnerWebView', () => {
|
||||
`,
|
||||
});
|
||||
render(<WrappedPluginRunnerWebView/>);
|
||||
|
||||
// Should load the plugin
|
||||
await waitFor(async () => {
|
||||
expect(PluginService.instance().pluginById(testPlugin.manifest.id)).toBeTruthy();
|
||||
});
|
||||
await waitForPluginToLoad(testPlugin);
|
||||
|
||||
// Should show the dialog
|
||||
await waitFor(async () => {
|
||||
const dom = await getWebViewDomById('joplin__PluginDialogWebView');
|
||||
const dom = await getUserWebViewDom();
|
||||
expect(dom.querySelector('h1').textContent).toBe('Test!');
|
||||
});
|
||||
});
|
||||
|
||||
test('should load a plugin that adds a panel', async () => {
|
||||
const testPlugin = await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: 'org.joplinapp.panel-test',
|
||||
}, {
|
||||
onStart: `
|
||||
const panels = joplin.views.panels;
|
||||
const handle = await panels.create('test-panel');
|
||||
await panels.setHtml(
|
||||
handle,
|
||||
'<h1>Panel content</h1><p>Test</p>',
|
||||
);
|
||||
|
||||
const commands = joplin.commands;
|
||||
await commands.register({
|
||||
name: 'hideTestPanel',
|
||||
label: 'Hide the test plugin panel',
|
||||
execute: async () => {
|
||||
await panels.hide(handle);
|
||||
},
|
||||
});
|
||||
|
||||
await commands.register({
|
||||
name: 'showTestPanel',
|
||||
execute: async () => {
|
||||
await panels.show(handle);
|
||||
},
|
||||
});
|
||||
`,
|
||||
});
|
||||
render(<WrappedPluginRunnerWebView/>);
|
||||
await waitForPluginToLoad(testPlugin);
|
||||
|
||||
act(() => {
|
||||
store.dispatch({ type: 'SET_PLUGIN_PANELS_DIALOG_VISIBLE', visible: true });
|
||||
});
|
||||
|
||||
const expectPanelVisible = async () => {
|
||||
const dom = await getUserWebViewDom();
|
||||
await waitFor(async () => {
|
||||
expect(dom.querySelector('h1').textContent).toBe('Panel content');
|
||||
});
|
||||
};
|
||||
await expectPanelVisible();
|
||||
|
||||
// Should hide the panel
|
||||
await act(() => CommandService.instance().execute('hideTestPanel'));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('webViewId')).toBeNull();
|
||||
});
|
||||
|
||||
// Should show the panel again
|
||||
await act(() => CommandService.instance().execute('showTestPanel'));
|
||||
await expectPanelVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.webViewContainer}>
|
||||
<View style={styles.webViewContainer} testID='plugin-tab-content'>
|
||||
<PluginUserWebView
|
||||
key={selectedTabId}
|
||||
themeId={props.themeId}
|
||||
|
||||
@@ -14,7 +14,7 @@ import NoteScreen from './components/screens/Note/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting, { } from '@joplin/lib/models/Setting';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import reducer, { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
||||
import { NotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
||||
import ShareExtension, { UnsubscribeShareListener } from './utils/ShareExtension';
|
||||
import handleShared from './utils/shareHandler';
|
||||
import { _, setLocale } from '@joplin/lib/locale';
|
||||
@@ -28,7 +28,6 @@ import NetInfo, { NetInfoSubscription } from '@react-native-community/netinfo';
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
import SafeAreaView from './components/SafeAreaView';
|
||||
const { connect, Provider } = require('react-redux');
|
||||
import fastDeepEqual = require('fast-deep-equal');
|
||||
import { Provider as PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
|
||||
import BackButtonService, { BackButtonHandler } from './services/BackButtonService';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
@@ -95,7 +94,6 @@ import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTh
|
||||
import PluginRunnerWebView from './components/plugins/PluginRunnerWebView';
|
||||
import { refreshFolders, scheduleRefreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
import ShareManager from './components/screens/ShareManager';
|
||||
import appDefaultState from './utils/appDefaultState';
|
||||
import { setDateFormat, setTimeFormat, setTimeLocale } from '@joplin/utils/time';
|
||||
import DialogManager from './components/DialogManager';
|
||||
import { AppState } from './utils/types';
|
||||
@@ -108,6 +106,7 @@ import NoteRevisionViewer from './components/screens/NoteRevisionViewer';
|
||||
import DocumentScanner from './components/screens/DocumentScanner/DocumentScanner';
|
||||
import buildStartupTasks from './utils/buildStartupTasks';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import appReducer from './utils/appReducer';
|
||||
|
||||
const logger = Logger.create('root');
|
||||
const perfLogger = PerformanceLogger.create();
|
||||
@@ -235,204 +234,6 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
||||
return result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const navHistory: any[] = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function historyCanGoBackTo(route: any) {
|
||||
if (route.routeName === 'Folder') return false;
|
||||
|
||||
// This is an intermediate screen that acts more like a modal -- it should be skipped in the
|
||||
// navigation history.
|
||||
if (route.routeName === 'DocumentScanner') return false;
|
||||
|
||||
// There's no point going back to these screens in general and, at least in OneDrive case,
|
||||
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
|
||||
if (route.routeName === 'OneDriveLogin') return false;
|
||||
if (route.routeName === 'DropboxLogin') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const appReducer = (state = appDefaultState, action: any) => {
|
||||
let newState = state;
|
||||
let historyGoingBack = false;
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
|
||||
case 'NAV_BACK':
|
||||
case 'NAV_GO':
|
||||
|
||||
if (action.type === 'NAV_BACK') {
|
||||
if (!navHistory.length) break;
|
||||
|
||||
const newAction = navHistory.pop();
|
||||
action = newAction ? newAction : navHistory.pop();
|
||||
|
||||
historyGoingBack = true;
|
||||
}
|
||||
|
||||
{
|
||||
const currentRoute = state.route;
|
||||
|
||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute)) {
|
||||
const previousRoute = navHistory.length && navHistory[navHistory.length - 1];
|
||||
const isDifferentRoute = !previousRoute || !fastDeepEqual(navHistory[navHistory.length - 1], currentRoute);
|
||||
|
||||
// Avoid multiple consecutive duplicate screens in the navigation history -- these can make
|
||||
// pressing "back" seem to have no effect.
|
||||
if (isDifferentRoute) {
|
||||
navHistory.push(currentRoute);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.clearHistory) {
|
||||
navHistory.splice(0, navHistory.length);
|
||||
}
|
||||
|
||||
newState = { ...state };
|
||||
|
||||
newState.selectedNoteHash = '';
|
||||
|
||||
if (action.routeName === 'Search') {
|
||||
newState.notesParentType = 'Search';
|
||||
}
|
||||
|
||||
if ('noteId' in action) {
|
||||
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
||||
}
|
||||
|
||||
if ('folderId' in action) {
|
||||
newState.selectedFolderId = action.folderId;
|
||||
newState.notesParentType = 'Folder';
|
||||
}
|
||||
|
||||
if ('tagId' in action) {
|
||||
newState.selectedTagId = action.tagId;
|
||||
newState.notesParentType = 'Tag';
|
||||
}
|
||||
|
||||
if ('smartFilterId' in action) {
|
||||
newState.smartFilterId = action.smartFilterId;
|
||||
newState.selectedSmartFilterId = action.smartFilterId;
|
||||
newState.notesParentType = 'SmartFilter';
|
||||
}
|
||||
|
||||
if ('itemType' in action) {
|
||||
newState.selectedItemType = action.itemType;
|
||||
}
|
||||
|
||||
if ('noteHash' in action) {
|
||||
newState.selectedNoteHash = action.noteHash;
|
||||
}
|
||||
|
||||
if ('sharedData' in action) {
|
||||
newState.sharedData = action.sharedData;
|
||||
} else {
|
||||
newState.sharedData = null;
|
||||
}
|
||||
|
||||
newState.route = action;
|
||||
newState.historyCanGoBack = !!navHistory.length;
|
||||
|
||||
logger.debug('Navigated to route:', newState.route?.routeName, 'with notesParentType:', newState.notesParentType);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SIDE_MENU_TOGGLE':
|
||||
|
||||
newState = { ...state };
|
||||
newState.showSideMenu = !newState.showSideMenu;
|
||||
break;
|
||||
|
||||
case 'SIDE_MENU_OPEN':
|
||||
|
||||
newState = { ...state };
|
||||
newState.showSideMenu = true;
|
||||
break;
|
||||
|
||||
case 'SIDE_MENU_CLOSE':
|
||||
|
||||
newState = { ...state };
|
||||
newState.showSideMenu = false;
|
||||
break;
|
||||
|
||||
case 'SET_PLUGIN_PANELS_DIALOG_VISIBLE':
|
||||
newState = { ...state };
|
||||
newState.showPanelsDialog = action.visible;
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_TOGGLE':
|
||||
|
||||
{
|
||||
newState = { ...state };
|
||||
|
||||
const noteId = action.id;
|
||||
const newSelectedNoteIds = state.selectedNoteIds.slice();
|
||||
const existingIndex = state.selectedNoteIds.indexOf(noteId);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
newSelectedNoteIds.splice(existingIndex, 1);
|
||||
} else {
|
||||
newSelectedNoteIds.push(noteId);
|
||||
}
|
||||
|
||||
newState.selectedNoteIds = newSelectedNoteIds;
|
||||
newState.noteSelectionEnabled = !!newSelectedNoteIds.length;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_START':
|
||||
|
||||
if (!state.noteSelectionEnabled) {
|
||||
newState = { ...state };
|
||||
newState.noteSelectionEnabled = true;
|
||||
newState.selectedNoteIds = [action.id];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_END':
|
||||
|
||||
newState = { ...state };
|
||||
newState.noteSelectionEnabled = false;
|
||||
newState.selectedNoteIds = [];
|
||||
break;
|
||||
|
||||
case 'NOTE_SIDE_MENU_OPTIONS_SET':
|
||||
|
||||
newState = { ...state };
|
||||
newState.noteSideMenuOptions = action.options;
|
||||
break;
|
||||
|
||||
case 'SET_SIDE_MENU_TOUCH_GESTURES_DISABLED':
|
||||
newState = { ...state };
|
||||
newState.disableSideMenuGestures = action.disableSideMenuGestures;
|
||||
break;
|
||||
|
||||
case 'MOBILE_DATA_WARNING_UPDATE':
|
||||
|
||||
newState = { ...state };
|
||||
newState.isOnMobileData = action.isOnMobileData;
|
||||
break;
|
||||
|
||||
case 'KEYBOARD_VISIBLE_CHANGE':
|
||||
newState = { ...state, keyboardVisible: action.visible };
|
||||
break;
|
||||
|
||||
case 'NOTE_EDITOR_VISIBLE_CHANGE':
|
||||
newState = { ...state, noteEditorVisible: action.visible };
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return reducer(newState, action) as AppState;
|
||||
};
|
||||
|
||||
const store = createStore(appReducer, applyMiddleware(generalMiddleware));
|
||||
storeDispatch = store.dispatch;
|
||||
|
||||
|
||||
207
packages/app-mobile/utils/appReducer.ts
Normal file
207
packages/app-mobile/utils/appReducer.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import reducer from '@joplin/lib/reducer';
|
||||
import { AppState } from './types';
|
||||
import appDefaultState from './appDefaultState';
|
||||
import fastDeepEqual = require('fast-deep-equal');
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('appReducer');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const navHistory: any[] = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function historyCanGoBackTo(route: any) {
|
||||
if (route.routeName === 'Folder') return false;
|
||||
|
||||
// This is an intermediate screen that acts more like a modal -- it should be skipped in the
|
||||
// navigation history.
|
||||
if (route.routeName === 'DocumentScanner') return false;
|
||||
|
||||
// There's no point going back to these screens in general and, at least in OneDrive case,
|
||||
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
|
||||
if (route.routeName === 'OneDriveLogin') return false;
|
||||
if (route.routeName === 'DropboxLogin') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const appReducer = (state = appDefaultState, action: any) => {
|
||||
let newState = state;
|
||||
let historyGoingBack = false;
|
||||
|
||||
try {
|
||||
switch (action.type) {
|
||||
|
||||
case 'NAV_BACK':
|
||||
case 'NAV_GO':
|
||||
|
||||
if (action.type === 'NAV_BACK') {
|
||||
if (!navHistory.length) break;
|
||||
|
||||
const newAction = navHistory.pop();
|
||||
action = newAction ? newAction : navHistory.pop();
|
||||
|
||||
historyGoingBack = true;
|
||||
}
|
||||
|
||||
{
|
||||
const currentRoute = state.route;
|
||||
|
||||
if (!historyGoingBack && historyCanGoBackTo(currentRoute)) {
|
||||
const previousRoute = navHistory.length && navHistory[navHistory.length - 1];
|
||||
const isDifferentRoute = !previousRoute || !fastDeepEqual(navHistory[navHistory.length - 1], currentRoute);
|
||||
|
||||
// Avoid multiple consecutive duplicate screens in the navigation history -- these can make
|
||||
// pressing "back" seem to have no effect.
|
||||
if (isDifferentRoute) {
|
||||
navHistory.push(currentRoute);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.clearHistory) {
|
||||
navHistory.splice(0, navHistory.length);
|
||||
}
|
||||
|
||||
newState = { ...state };
|
||||
|
||||
newState.selectedNoteHash = '';
|
||||
|
||||
if (action.routeName === 'Search') {
|
||||
newState.notesParentType = 'Search';
|
||||
}
|
||||
|
||||
if ('noteId' in action) {
|
||||
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
||||
}
|
||||
|
||||
if ('folderId' in action) {
|
||||
newState.selectedFolderId = action.folderId;
|
||||
newState.notesParentType = 'Folder';
|
||||
}
|
||||
|
||||
if ('tagId' in action) {
|
||||
newState.selectedTagId = action.tagId;
|
||||
newState.notesParentType = 'Tag';
|
||||
}
|
||||
|
||||
if ('smartFilterId' in action) {
|
||||
newState.smartFilterId = action.smartFilterId;
|
||||
newState.selectedSmartFilterId = action.smartFilterId;
|
||||
newState.notesParentType = 'SmartFilter';
|
||||
}
|
||||
|
||||
if ('itemType' in action) {
|
||||
newState.selectedItemType = action.itemType;
|
||||
}
|
||||
|
||||
if ('noteHash' in action) {
|
||||
newState.selectedNoteHash = action.noteHash;
|
||||
}
|
||||
|
||||
if ('sharedData' in action) {
|
||||
newState.sharedData = action.sharedData;
|
||||
} else {
|
||||
newState.sharedData = null;
|
||||
}
|
||||
|
||||
newState.route = action;
|
||||
newState.historyCanGoBack = !!navHistory.length;
|
||||
|
||||
logger.debug('Navigated to route:', newState.route?.routeName, 'with notesParentType:', newState.notesParentType);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SIDE_MENU_TOGGLE':
|
||||
|
||||
newState = { ...state };
|
||||
newState.showSideMenu = !newState.showSideMenu;
|
||||
break;
|
||||
|
||||
case 'SIDE_MENU_OPEN':
|
||||
|
||||
newState = { ...state };
|
||||
newState.showSideMenu = true;
|
||||
break;
|
||||
|
||||
case 'SIDE_MENU_CLOSE':
|
||||
|
||||
newState = { ...state };
|
||||
newState.showSideMenu = false;
|
||||
break;
|
||||
|
||||
case 'SET_PLUGIN_PANELS_DIALOG_VISIBLE':
|
||||
newState = { ...state };
|
||||
newState.showPanelsDialog = action.visible;
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_TOGGLE':
|
||||
|
||||
{
|
||||
newState = { ...state };
|
||||
|
||||
const noteId = action.id;
|
||||
const newSelectedNoteIds = state.selectedNoteIds.slice();
|
||||
const existingIndex = state.selectedNoteIds.indexOf(noteId);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
newSelectedNoteIds.splice(existingIndex, 1);
|
||||
} else {
|
||||
newSelectedNoteIds.push(noteId);
|
||||
}
|
||||
|
||||
newState.selectedNoteIds = newSelectedNoteIds;
|
||||
newState.noteSelectionEnabled = !!newSelectedNoteIds.length;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_START':
|
||||
|
||||
if (!state.noteSelectionEnabled) {
|
||||
newState = { ...state };
|
||||
newState.noteSelectionEnabled = true;
|
||||
newState.selectedNoteIds = [action.id];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_END':
|
||||
|
||||
newState = { ...state };
|
||||
newState.noteSelectionEnabled = false;
|
||||
newState.selectedNoteIds = [];
|
||||
break;
|
||||
|
||||
case 'NOTE_SIDE_MENU_OPTIONS_SET':
|
||||
|
||||
newState = { ...state };
|
||||
newState.noteSideMenuOptions = action.options;
|
||||
break;
|
||||
|
||||
case 'SET_SIDE_MENU_TOUCH_GESTURES_DISABLED':
|
||||
newState = { ...state };
|
||||
newState.disableSideMenuGestures = action.disableSideMenuGestures;
|
||||
break;
|
||||
|
||||
case 'MOBILE_DATA_WARNING_UPDATE':
|
||||
|
||||
newState = { ...state };
|
||||
newState.isOnMobileData = action.isOnMobileData;
|
||||
break;
|
||||
|
||||
case 'KEYBOARD_VISIBLE_CHANGE':
|
||||
newState = { ...state, keyboardVisible: action.visible };
|
||||
break;
|
||||
|
||||
case 'NOTE_EDITOR_VISIBLE_CHANGE':
|
||||
newState = { ...state, noteEditorVisible: action.visible };
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return reducer(newState, action) as AppState;
|
||||
};
|
||||
|
||||
export default appReducer;
|
||||
@@ -1,15 +1,16 @@
|
||||
import reducer from '@joplin/lib/reducer';
|
||||
import { createStore } from 'redux';
|
||||
import appDefaultState from '../appDefaultState';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { AppState } from '../types';
|
||||
import appReducer from '../appReducer';
|
||||
|
||||
const testReducer = (state: AppState|undefined, action: unknown): AppState => {
|
||||
state ??= {
|
||||
...appDefaultState,
|
||||
settings: Setting.toPlainObject(),
|
||||
};
|
||||
return { ...state, ...reducer(state, action) };
|
||||
|
||||
return { ...state, ...appReducer(state, action) };
|
||||
};
|
||||
|
||||
const createMockReduxStore = () => {
|
||||
|
||||
Reference in New Issue
Block a user