Files
openhuman/app/test/Mnemonic.test.tsx
T
Steven EnamakelandGitHub ef708bdd20 refactor(core): move app state ownership into the Rust core (#320)
* refactor(core): integrate CoreStateProvider and streamline state management

- Replaced UserProvider with CoreStateProvider in App component to centralize state management.
- Updated various components to utilize the new CoreStateProvider for accessing session tokens and user data.
- Refactored hooks and components to eliminate direct Redux store dependencies, enhancing modularity and maintainability.
- Introduced a new core state management structure to improve the handling of user authentication and onboarding states.

This refactor aims to simplify state access across the application and improve overall code clarity.

* fix: restore polyfills import and clean up component imports

- Reintroduced the import of polyfills in main.tsx to ensure compatibility.
- Adjusted import statements in PrivacyPanel and SocketProvider for consistency.
- Simplified conditional expressions in TeamInvitesPanel and TeamMembersPanel for better readability.
- Refactored CoreStateProvider and store.ts to streamline state initialization.
- Enhanced formatting in various files for improved code clarity and maintainability.

* refactor(tests): streamline onboarding and protected route tests

- Updated OnboardingOverlay tests to utilize mock state management and simplify assertions.
- Refactored ProtectedRoute tests to improve readability and ensure consistent use of mock state.
- Enhanced teamApi tests to replace direct API calls with core RPC method calls, improving test isolation and clarity.
- Adjusted socketSelectors tests to utilize a centralized core state snapshot for better state management during tests.

* refactor(onboarding): simplify onboarding state management and improve loading logic

- Removed unnecessary local state for onboarding completion in OnboardingOverlay, directly utilizing snapshot data.
- Enhanced loading logic to prevent unnecessary renders and improve user experience during onboarding.
- Updated PrivacyPanel to handle analytics consent persistence with error handling.
- Refactored TeamManagementPanel and TeamPanel to improve team data fetching logic and loading states.
- Streamlined CoreStateProvider to manage session token synchronization and state updates more effectively.

* chore(deps): update tempfile dependency and refactor app state management

- Added `tempfile` dependency to Cargo.toml for improved temporary file handling.
- Refactored app state loading and saving functions to enhance error handling and ensure data integrity.
- Introduced quarantine mechanism for corrupted app state files to prevent application crashes.
- Updated URL parsing logic to ensure proper formatting of API URLs.
- Adjusted type definitions in schemas for better clarity and consistency.

* refactor(tests): simplify mock state usage in onboarding and protected route tests

- Consolidated mock state management in OnboardingOverlay and ProtectedRoute tests for improved readability.
- Streamlined assertions by reducing unnecessary lines in test setups.
- Enhanced consistency in mock return values across tests to ensure clarity and maintainability.

* refactor(tests): enhance PublicRoute tests with mock state and routing

- Updated PublicRoute tests to utilize MemoryRouter for improved routing simulation.
- Simplified mock state management by integrating `useCoreState` for user authentication scenarios.
- Streamlined test assertions and removed redundant preloaded state configurations for clarity and maintainability.

* refactor(tests): streamline mock state usage in PublicRoute and Mnemonic tests

- Simplified mock state management in PublicRoute and Mnemonic tests for improved readability.
- Consolidated mock return values to reduce redundancy and enhance clarity in test setups.
- Improved consistency in the usage of `useCoreState` across test files.

* Refactor billing components for improved readability

- Cleaned up import statements in BillingPanel.tsx for better organization.
- Enhanced formatting of billing plan descriptions in billingHelpers.ts for consistency.
- Improved readability of conditional checks in BillingPanel component.

* Refactor API endpoint handling for clarity and consistency

- Updated references from `/settings` to `/auth/me` in various modules to standardize user authentication flows.
- Renamed `parse_settings_response_json` to `parse_api_response_json` for improved clarity in response handling.
- Adjusted user ID extraction functions to reflect the new endpoint structure, enhancing maintainability across the codebase.
- Updated test cases to align with the new endpoint naming conventions, ensuring consistency in API interactions.
2026-04-04 03:20:18 -07:00

680 lines
23 KiB
TypeScript

/// <reference types="@testing-library/jest-dom/vitest" />
/**
* Tests for the Mnemonic page.
*
* Coverage areas:
* - Initial render: generate mode UI, word grid, buttons
* - Copy to clipboard (success + fallback paths)
* - Confirmation checkbox gates the Continue button
* - Mode switch: generate ↔ import, state resets on switch
* - Import mode: word input, auto-advance, backspace navigation, paste
* - Validation: incomplete phrase, invalid phrase, valid phrase
* - handleContinue — generate mode: happy path, no user, crypto error
* - handleContinue — import mode: happy path, validation failure, no user
* - Loading state during continue
* - Navigation to /home on success
* - Core-state setEncryptionKey persistence
*/
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import Mnemonic from '../src/pages/Mnemonic';
import { renderWithProviders } from '../src/test/test-utils';
import type { User } from '../src/types/api';
// ---------------------------------------------------------------------------
// Module mocks
// ---------------------------------------------------------------------------
const FIXED_MNEMONIC =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon ' +
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art';
const {
mockGenerateMnemonicPhrase,
mockValidateMnemonicPhrase,
mockDeriveAesKey,
mockDeriveEvm,
mockSetWalletAddress,
mockSetEncryptionKey,
mockUseCoreState,
} = vi.hoisted(() => ({
mockGenerateMnemonicPhrase: vi.fn(() => FIXED_MNEMONIC),
mockValidateMnemonicPhrase: vi.fn(() => true),
mockDeriveAesKey: vi.fn(() => 'aes-key-hex'),
mockDeriveEvm: vi.fn(() => '0xDeAdBeEf'),
mockSetWalletAddress: vi.fn().mockResolvedValue(undefined),
mockSetEncryptionKey: vi.fn().mockResolvedValue(undefined),
mockUseCoreState: vi.fn(),
}));
vi.mock('../src/utils/cryptoKeys', () => ({
MNEMONIC_GENERATE_WORD_COUNT: 24,
generateMnemonicPhrase: mockGenerateMnemonicPhrase,
validateMnemonicPhrase: mockValidateMnemonicPhrase,
deriveAesKeyFromMnemonic: mockDeriveAesKey,
deriveEvmAddressFromMnemonic: mockDeriveEvm,
}));
vi.mock('../src/lib/skills/manager', () => ({
skillManager: { setWalletAddress: mockSetWalletAddress },
}));
vi.mock('../src/providers/CoreStateProvider', () => ({ useCoreState: () => mockUseCoreState() }));
// LottieAnimation makes network calls; stub it out
vi.mock('../src/components/LottieAnimation', () => ({
default: () => <div data-testid="lottie" />,
}));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const WORD_COUNT = 24;
const FIXED_WORDS = FIXED_MNEMONIC.split(' '); // 24 words
/** User with a valid _id so the "user not loaded" guard passes. */
const mockUser: Partial<User> = { _id: 'user-123', username: 'tester' };
/** Render with a user already in the store. */
const renderWithUser = () => renderWithProviders(<Mnemonic />);
/** Render without a user in the store (unauthenticated). */
const renderWithoutUser = () => renderWithProviders(<Mnemonic />);
/** Switch to import mode. */
const switchToImport = () => fireEvent.click(screen.getByText('I already have a recovery phrase'));
/** Fill all 24 import inputs with the words from `phrase`. */
const fillAllImportWords = (phrase = FIXED_MNEMONIC) => {
const words = phrase.split(' ');
const inputs = screen.getAllByRole('textbox');
// Paste into the first field to trigger multi-word paste handling
fireEvent.change(inputs[0], { target: { value: words.join(' ') } });
};
/** Get the Continue button. */
const continueButton = () => screen.getByRole('button', { name: /import & continue|let's go/i });
beforeEach(() => {
mockGenerateMnemonicPhrase.mockClear();
mockValidateMnemonicPhrase.mockClear();
mockDeriveAesKey.mockClear();
mockDeriveEvm.mockClear();
mockSetWalletAddress.mockClear();
mockSetEncryptionKey.mockClear();
mockUseCoreState.mockReturnValue({
snapshot: { currentUser: mockUser, sessionToken: 'jwt-token' },
setEncryptionKey: mockSetEncryptionKey,
});
});
// ---------------------------------------------------------------------------
// Generate mode — initial render
// ---------------------------------------------------------------------------
describe('Mnemonic — generate mode: initial render', () => {
it('renders the "Your Recovery Phrase" heading', () => {
renderWithUser();
expect(screen.getByText('Your Recovery Phrase')).toBeInTheDocument();
});
it('renders all 24 words from the generated mnemonic', () => {
renderWithUser();
for (const word of FIXED_WORDS) {
expect(screen.getAllByText(word).length).toBeGreaterThan(0);
}
});
it('renders 24 numbered word tiles', () => {
renderWithUser();
expect(screen.getByText('1.')).toBeInTheDocument();
expect(screen.getByText('24.')).toBeInTheDocument();
});
it('renders the Copy to Clipboard button', () => {
renderWithUser();
expect(screen.getByRole('button', { name: /copy to clipboard/i })).toBeInTheDocument();
});
it('renders the amber warning notice', () => {
renderWithUser();
expect(screen.getByText(/can never be recovered if lost/i)).toBeInTheDocument();
});
it('renders the confirmation checkbox unchecked', () => {
renderWithUser();
expect(screen.getByRole('checkbox')).not.toBeChecked();
});
it('renders the Continue button disabled before confirmation', () => {
renderWithUser();
expect(continueButton()).toBeDisabled();
});
it('renders the "I already have a recovery phrase" link', () => {
renderWithUser();
expect(screen.getByText('I already have a recovery phrase')).toBeInTheDocument();
});
});
// ---------------------------------------------------------------------------
// Generate mode — confirmation checkbox
// ---------------------------------------------------------------------------
describe('Mnemonic — generate mode: confirmation checkbox', () => {
it('enables the Continue button when checkbox is checked', () => {
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
expect(continueButton()).toBeEnabled();
});
it('disables the Continue button again when checkbox is unchecked', () => {
renderWithUser();
const checkbox = screen.getByRole('checkbox');
fireEvent.click(checkbox);
fireEvent.click(checkbox);
expect(continueButton()).toBeDisabled();
});
});
// ---------------------------------------------------------------------------
// Generate mode — copy to clipboard
// ---------------------------------------------------------------------------
describe('Mnemonic — generate mode: copy to clipboard', () => {
it('calls navigator.clipboard.writeText with the full mnemonic', async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.assign(navigator, { clipboard: { writeText } });
renderWithUser();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /copy to clipboard/i }));
});
expect(writeText).toHaveBeenCalledWith(FIXED_MNEMONIC);
});
it('shows "Copied to Clipboard" after clicking copy', async () => {
Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
renderWithUser();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /copy to clipboard/i }));
});
await waitFor(() => expect(screen.getByText('Copied to Clipboard')).toBeInTheDocument());
});
it('resets "Copied" text back to "Copy to Clipboard" after 3 s', async () => {
vi.useFakeTimers();
Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
renderWithUser();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /copy to clipboard/i }));
});
// Flush the resolved clipboard promise so setCopied(true) fires
await act(async () => {
await vi.runAllTimersAsync();
});
// Now the 3-second reset timer has also been run
expect(screen.queryByText('Copied to Clipboard')).not.toBeInTheDocument();
vi.useRealTimers();
});
it('uses execCommand fallback when clipboard API throws', async () => {
Object.assign(navigator, {
clipboard: { writeText: vi.fn().mockRejectedValue(new Error('blocked')) },
});
// jsdom does not implement execCommand — define it so we can spy
if (!document.execCommand) {
Object.defineProperty(document, 'execCommand', {
value: vi.fn().mockReturnValue(true),
writable: true,
configurable: true,
});
}
const execCommand = vi.spyOn(document, 'execCommand').mockReturnValue(true);
renderWithUser();
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: /copy to clipboard/i }));
});
await waitFor(() => expect(screen.getByText('Copied to Clipboard')).toBeInTheDocument());
expect(execCommand).toHaveBeenCalledWith('copy');
execCommand.mockRestore();
});
});
// ---------------------------------------------------------------------------
// Mode switching
// ---------------------------------------------------------------------------
describe('Mnemonic — mode switching', () => {
it('switches to import mode on "I already have a recovery phrase" click', () => {
renderWithUser();
switchToImport();
expect(screen.getByText('Import Recovery Phrase')).toBeInTheDocument();
});
it('shows 24 text inputs in import mode', () => {
renderWithUser();
switchToImport();
expect(screen.getAllByRole('textbox')).toHaveLength(WORD_COUNT);
});
it('switches back to generate mode on "Generate a new recovery phrase instead"', () => {
renderWithUser();
switchToImport();
fireEvent.click(screen.getByText('Generate a new recovery phrase instead'));
expect(screen.getByText('Your Recovery Phrase')).toBeInTheDocument();
});
it('resets error when switching modes', async () => {
renderWithUser();
// Trigger an error in generate mode (click continue without confirming)
fireEvent.click(continueButton()); // disabled, won't trigger, so force via import mode
// Switch to import mode and back — confirmed state should reset
switchToImport();
expect(screen.queryByText(/please enter all/i)).not.toBeInTheDocument();
});
it('resets confirmation when switching from generate to import and back', () => {
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
expect(continueButton()).toBeEnabled();
switchToImport();
fireEvent.click(screen.getByText('Generate a new recovery phrase instead'));
// Confirmed state is reset — Continue should be disabled again
expect(continueButton()).toBeDisabled();
});
});
// ---------------------------------------------------------------------------
// Import mode — word input behaviour
// ---------------------------------------------------------------------------
describe('Mnemonic — import mode: word input', () => {
beforeEach(() => {
renderWithUser();
switchToImport();
});
it('updates input value when a word is typed', () => {
const inputs = screen.getAllByRole('textbox');
fireEvent.change(inputs[0], { target: { value: 'abandon' } });
expect((inputs[0] as HTMLInputElement).value).toBe('abandon');
});
it('lowercases the entered word', () => {
const inputs = screen.getAllByRole('textbox');
fireEvent.change(inputs[0], { target: { value: 'ABANDON' } });
expect((inputs[0] as HTMLInputElement).value).toBe('abandon');
});
it('Continue button stays disabled until all 24 words are filled', () => {
expect(continueButton()).toBeDisabled();
const inputs = screen.getAllByRole('textbox');
// Fill only 23 words
for (let i = 0; i < 23; i++) {
fireEvent.change(inputs[i], { target: { value: 'abandon' } });
}
expect(continueButton()).toBeDisabled();
});
it('Continue button becomes enabled when all 24 words are filled (via paste)', () => {
fillAllImportWords();
expect(continueButton()).toBeEnabled();
});
it('distributes pasted multi-word phrase across inputs starting at index 0', () => {
const inputs = screen.getAllByRole('textbox');
fireEvent.change(inputs[0], { target: { value: FIXED_WORDS.join(' ') } });
// After paste the first input gets the first word
expect((inputs[0] as HTMLInputElement).value).toBe(FIXED_WORDS[0]);
});
it('distributes pasted phrase starting from a non-zero index', () => {
const inputs = screen.getAllByRole('textbox');
const remaining = FIXED_WORDS.slice(1).join(' ');
fireEvent.change(inputs[1], { target: { value: remaining } });
expect((inputs[1] as HTMLInputElement).value).toBe(FIXED_WORDS[1]);
expect((inputs[2] as HTMLInputElement).value).toBe(FIXED_WORDS[2]);
});
});
// ---------------------------------------------------------------------------
// Import mode — keyboard navigation
// ---------------------------------------------------------------------------
describe('Mnemonic — import mode: keyboard navigation', () => {
beforeEach(() => {
renderWithUser();
switchToImport();
});
it('does not move focus backward on Backspace when input has text', () => {
const inputs = screen.getAllByRole('textbox');
fireEvent.change(inputs[1], { target: { value: 'test' } });
inputs[1].focus();
fireEvent.keyDown(inputs[1], { key: 'Backspace' });
// focus should stay on inputs[1]
expect(document.activeElement).toBe(inputs[1]);
});
});
// ---------------------------------------------------------------------------
// Import mode — validation
// ---------------------------------------------------------------------------
describe('Mnemonic — import mode: validation', () => {
beforeEach(() => {
renderWithUser();
switchToImport();
});
it('shows error when the phrase fails BIP39 validation after all 24 words are entered', async () => {
// The Continue button is only enabled when all 24 inputs are filled, so the
// "please enter all words" branch is unreachable via normal UI.
// The reachable validation error is the invalid-phrase message.
mockValidateMnemonicPhrase.mockReturnValueOnce(false);
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText(/invalid recovery phrase/i)).toBeInTheDocument());
});
it('shows error when the 24-word phrase is invalid (BIP39)', async () => {
mockValidateMnemonicPhrase.mockReturnValueOnce(false);
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText(/invalid recovery phrase/i)).toBeInTheDocument());
});
it('shows "Valid recovery phrase" text when phrase passes BIP39 validation', async () => {
mockValidateMnemonicPhrase.mockReturnValue(true);
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText('Valid recovery phrase')).toBeInTheDocument());
});
});
// ---------------------------------------------------------------------------
// handleContinue — generate mode
// ---------------------------------------------------------------------------
describe('Mnemonic — handleContinue: generate mode', () => {
it('shows loading text while processing', async () => {
// Make setWalletAddress hang so we can observe the loading state
let resolveWallet!: () => void;
mockSetWalletAddress.mockReturnValueOnce(
new Promise<void>(res => {
resolveWallet = res;
})
);
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText('Securing Your Data...')).toBeInTheDocument());
await act(async () => {
resolveWallet();
});
});
it('calls setEncryptionKey with the derived AES key', async () => {
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(mockSetWalletAddress).toHaveBeenCalled());
expect(mockSetEncryptionKey).toHaveBeenCalledWith('aes-key-hex');
});
it('calls deriveAesKeyFromMnemonic and deriveEvmAddressFromMnemonic with the mnemonic', async () => {
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(mockSetWalletAddress).toHaveBeenCalled());
expect(mockDeriveAesKey).toHaveBeenCalledWith(FIXED_MNEMONIC);
expect(mockDeriveEvm).toHaveBeenCalledWith(FIXED_MNEMONIC);
});
it('calls skillManager.setWalletAddress with the derived EVM address', async () => {
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(mockSetWalletAddress).toHaveBeenCalledWith('0xDeAdBeEf'));
});
it('shows "User not loaded" error when user._id is missing', async () => {
mockUseCoreState.mockReturnValue({
snapshot: { currentUser: null, sessionToken: 'jwt-token' },
setEncryptionKey: mockSetEncryptionKey,
});
renderWithoutUser();
// The checkbox click + continue in generate mode with no user
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getAllByText(/user not loaded/i).length).toBeGreaterThan(0));
});
it('shows an error message when deriveAesKeyFromMnemonic throws', async () => {
mockDeriveAesKey.mockImplementationOnce(() => {
throw new Error('crypto failure');
});
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText('crypto failure')).toBeInTheDocument());
});
it('does not navigate when unconfirmed in generate mode', async () => {
renderWithUser();
// Do NOT check the checkbox
await act(async () => {
fireEvent.click(continueButton());
});
// No dispatch should have happened
await new Promise(r => setTimeout(r, 50));
expect(mockSetWalletAddress).not.toHaveBeenCalled();
expect(mockSetEncryptionKey).not.toHaveBeenCalled();
});
});
// ---------------------------------------------------------------------------
// handleContinue — import mode
// ---------------------------------------------------------------------------
describe('Mnemonic — handleContinue: import mode', () => {
beforeEach(() => {
mockValidateMnemonicPhrase.mockReturnValue(true);
});
it('derives keys from the imported phrase and navigates to /home', async () => {
renderWithUser();
switchToImport();
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(mockSetWalletAddress).toHaveBeenCalledWith('0xDeAdBeEf'));
expect(mockDeriveAesKey).toHaveBeenCalledWith(FIXED_MNEMONIC);
});
it('calls setEncryptionKey on successful import', async () => {
renderWithUser();
switchToImport();
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(mockSetWalletAddress).toHaveBeenCalled());
expect(mockSetEncryptionKey).toHaveBeenCalledWith('aes-key-hex');
});
it('does not call deriveAesKey when validation fails', async () => {
mockValidateMnemonicPhrase.mockReturnValueOnce(false);
renderWithUser();
switchToImport();
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText(/invalid recovery phrase/i)).toBeInTheDocument());
expect(mockDeriveAesKey).not.toHaveBeenCalled();
});
it('shows "User not loaded" error when user is absent during import', async () => {
mockValidateMnemonicPhrase.mockReturnValue(true);
mockUseCoreState.mockReturnValue({
snapshot: { currentUser: null, sessionToken: 'jwt-token' },
setEncryptionKey: mockSetEncryptionKey,
});
renderWithProviders(<Mnemonic />);
switchToImport();
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText(/user not loaded/i)).toBeInTheDocument());
});
it('shows an error when skillManager.setWalletAddress throws during import', async () => {
mockSetWalletAddress.mockRejectedValueOnce(new Error('wallet error'));
renderWithUser();
switchToImport();
fillAllImportWords();
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText('wallet error')).toBeInTheDocument());
});
});
// ---------------------------------------------------------------------------
// Loading state during continue
// ---------------------------------------------------------------------------
describe('Mnemonic — loading state', () => {
it('disables Continue button while loading', async () => {
let resolveWallet!: () => void;
mockSetWalletAddress.mockReturnValueOnce(
new Promise<void>(res => {
resolveWallet = res;
})
);
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
const btn = continueButton();
await act(async () => {
fireEvent.click(btn);
});
await waitFor(() => expect(screen.getByText('Securing Your Data...')).toBeInTheDocument());
expect(btn).toBeDisabled();
await act(async () => {
resolveWallet();
});
});
it('restores button label after an error', async () => {
mockDeriveAesKey.mockImplementationOnce(() => {
throw new Error('oops');
});
renderWithUser();
fireEvent.click(screen.getByRole('checkbox'));
await act(async () => {
fireEvent.click(continueButton());
});
await waitFor(() => expect(screen.getByText('oops')).toBeInTheDocument());
expect(screen.queryByText('Securing Your Data...')).not.toBeInTheDocument();
});
});
// ---------------------------------------------------------------------------
// Provider configuration sanity checks
// ---------------------------------------------------------------------------
describe('Mnemonic — providerConfigs sanity', () => {
it('calls generateMnemonicPhrase exactly once on mount', () => {
mockGenerateMnemonicPhrase.mockClear();
renderWithUser();
// useMemo with [] dep runs once per render
expect(mockGenerateMnemonicPhrase).toHaveBeenCalledTimes(1);
});
it('does not call generateMnemonicPhrase again after mode switch', () => {
mockGenerateMnemonicPhrase.mockClear();
renderWithUser();
const callsBefore = mockGenerateMnemonicPhrase.mock.calls.length;
switchToImport();
fireEvent.click(screen.getByText('Generate a new recovery phrase instead'));
expect(mockGenerateMnemonicPhrase.mock.calls.length).toBe(callsBefore);
});
});