一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
219 行
7.9 KiB
TypeScript
219 行
7.9 KiB
TypeScript
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<AppState>;
|
|
}
|
|
|
|
const WarningBannerWrapper: React.FC<WrapperProps> = ({ store }) => {
|
|
return <TestProviderStack store={store}>
|
|
<WarningBanner />
|
|
</TestProviderStack>;
|
|
};
|
|
|
|
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(<WarningBannerWrapper store={mock.store}/>);
|
|
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(<WarningBannerWrapper store={mock.store}/>);
|
|
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(<WarningBannerWrapper store={store}/>);
|
|
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(<WarningBannerWrapper store={mock.store}/>);
|
|
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(<WarningBannerWrapper store={mock.store}/>);
|
|
|
|
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(<WarningBannerWrapper store={mock.store}/>);
|
|
|
|
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(<TestProviderStack store={store}><WarningBanner/></TestProviderStack>);
|
|
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');
|
|
});
|
|
});
|