import * as React from 'react'; import WarningBanner from './WarningBanner'; import Setting from '@joplin/lib/models/Setting'; import { act, fireEvent, render, screen, userEvent } from '../../utils/testing/testingLibrary'; import { ShareInvitation, ShareUserStatus } from '@joplin/lib/services/share/reducer'; import makeShareInvitation from '@joplin/lib/testing/share/makeMockShareInvitation'; import { encryptionService, setupDatabaseAndSynchronizer, switchClient, synchronizer, synchronizerStart } from '@joplin/lib/testing/test-utils'; import { fetchSyncInfo, uploadSyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; import TestProviderStack from '../testing/TestProviderStack'; import { AppState } from '../../utils/types'; import { Store } from 'redux'; import createMockReduxStore from '../../utils/testing/createMockReduxStore'; import setupGlobalStore from '../../utils/testing/setupGlobalStore'; import MasterKey from '@joplin/lib/models/MasterKey'; import shim from '@joplin/lib/shim'; interface WrapperProps { store: Store; } const WarningBannerWrapper: React.FC = ({ store }) => { return ; }; const createMockStore = () => { const store = createMockReduxStore(); setupGlobalStore(store); return { store, getRouteName: () => store.getState().route.routeName, simulateNotLoadedMasterKey: async () => { const key = await MasterKey.save(await encryptionService().generateMasterKey('111111')); act(() => { store.dispatch({ type: 'MASTERKEY_ADD_NOT_LOADED', id: key.id, }); }); }, setShareInvitations: (invitations: ShareInvitation[]) => { act(() => { store.dispatch({ type: 'SHARE_INVITATION_SET', shareInvitations: invitations, }); }); }, setIsProcessingShareInvitations: (processing: boolean) => { act(() => { store.dispatch({ type: 'SHARE_INVITATION_RESPONSE_PROCESSING', value: processing, }); }); }, setMustAuthenticate: () => { act(() => { store.dispatch({ type: 'MUST_AUTHENTICATE', value: true, }); }); }, }; }; describe('WarningBanner', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(0); await switchClient(0); jest.useFakeTimers(); }); test('the missing master key alert should link to the encryption config screen', async () => { const mock = createMockStore(); const defaultRoute = mock.getRouteName(); await mock.simulateNotLoadedMasterKey(); render(); expect(await screen.findAllByTestId('warning-box')).toHaveLength(1); expect(mock.getRouteName()).toBe(defaultRoute); const masterKeyWarning = screen.getByText(/decryption password/); const user = userEvent.setup(); await user.press(masterKeyWarning); expect(mock.getRouteName()).toBe('EncryptionConfig'); }); test.each([ [makeShareInvitation('Test user', 'email@example.com', ShareUserStatus.Waiting), true], [makeShareInvitation('Test user', 'email@example.com', ShareUserStatus.Accepted), false], [makeShareInvitation('Test user', 'email@example.com', ShareUserStatus.Rejected), false], ])('should display a warning banner when there is an incoming share (case %#)', (invitation, shouldShow) => { const mock = createMockStore(); mock.setShareInvitations([invitation]); render(); const checkShownState = () => { if (shouldShow) { expect(screen.getByText(/would like to share a notebook/)).toBeVisible(); } else { expect(screen.queryByText(/would like to share a notebook/)).toBeNull(); } }; checkShownState(); // Should not be affected by additional rejected/accepted invitations for (const inviteType of [ShareUserStatus.Accepted, ShareUserStatus.Rejected]) { mock.setShareInvitations([invitation, makeShareInvitation('A', 'a@example.com', inviteType)]); checkShownState(); } }); test('should not display a share warning banner while processing shares', () => { const invitations = [makeShareInvitation('Test Name', 'email@example.com', ShareUserStatus.Waiting)]; const query = /Test Name \(email@example\.com\) would like to share a notebook/; const { store, setShareInvitations, setIsProcessingShareInvitations } = createMockStore(); setShareInvitations(invitations); setIsProcessingShareInvitations(false); render(); expect(screen.getByText(query)).toBeVisible(); setIsProcessingShareInvitations(true); expect(screen.queryByText(query)).toBeNull(); setIsProcessingShareInvitations(false); expect(screen.getByText(query)).toBeVisible(); }); test('the note lock key conflict banner should link to the migration screen', () => { Setting.setValue('featureFlag.noteLock', true); Setting.setValue('noteLock.conflictNoteLockKey', { noteLockKey: { id: 'sync-target-key' }, syncMigrationId: 'lineage' }); const mock = createMockStore(); render(); const banner = screen.getByText(/different note lock key/); expect(banner).toBeVisible(); fireEvent.press(banner); expect(mock.getRouteName()).toBe('NoteLockMigration'); }); test('the note lock migration banner should follow the run and be dismissed once it has completed', () => { Setting.setValue('featureFlag.noteLock', true); Setting.setValue('noteLock.conflictNoteLockKey', { noteLockKey: { id: 'sync-target-key' }, syncMigrationId: 'lineage' }); const mock = createMockStore(); const setStatus = (value: unknown) => act(() => { mock.store.dispatch({ type: 'NOTE_LOCK_MIGRATION_STATUS_SET', value }); }); render(); setStatus({ running: true, failed: 0, skipped: 0 }); expect(screen.getByText(/Migration is in progress/)).toBeVisible(); expect(screen.queryByText(/different note lock key/)).toBeNull(); setStatus({ running: false, failed: 2, skipped: 0 }); expect(screen.getByText(/2 locked notes could not be migrated/)).toBeVisible(); const showMessageBox = jest.spyOn(shim, 'showMessageBox').mockResolvedValue(1); fireEvent.press(screen.getByText('Skip')); expect(showMessageBox).toHaveBeenCalled(); showMessageBox.mockRestore(); fireEvent.press(screen.getByText('Retry')); expect(mock.getRouteName()).toBe('NoteLockMigration'); setStatus({ running: false, failed: 0, skipped: 1 }); expect(screen.getByText(/has completed.*1 locked note was skipped/)).toBeVisible(); fireEvent.press(screen.getByText(/has completed/)); expect(mock.store.getState().noteLockMigrationStatus).toBeNull(); }); test('invalid credentials banner for Joplin Cloud should link to a login screen', () => { Setting.setValue('sync.target', 10); const mock = createMockStore(); mock.setMustAuthenticate(); render(); const buttonName = 'Your Joplin Cloud credentials are invalid, please login.'; const button = screen.getByRole('button', { name: buttonName }); expect(button).toBeVisible(); fireEvent.press(button); // Should open the login screen expect(mock.getRouteName()).toBe('JoplinCloudLogin'); // Should hide the warning banner expect(screen.queryByRole('button', { name: buttonName })).toBeNull(); }); test('should show the sync target minimum version in the upgrade app banner', async () => { await setupDatabaseAndSynchronizer(0); await switchClient(0); await synchronizerStart(); const store = createMockReduxStore(); setupGlobalStore(store); const remoteInfo = await fetchSyncInfo(synchronizer().api()); remoteInfo.appMinVersion = '3.9.0'; await uploadSyncInfo(synchronizer().api(), remoteInfo); synchronizer().dispatch = store.dispatch; await synchronizerStart(); render(); expect(screen.getByTestId('warning-box')).toHaveTextContent('In order to synchronise, Please upgrade your application to version 3.9.0: Update it from the App Store'); }); });