文件
mynode/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx
chen 301194963b
一些检查失败了
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
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

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');
});
});