1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-04-14 11:18:47 +02:00

Chore: Reduce mobile note screen test flakiness (#11145)

This commit is contained in:
Henry Heino 2024-09-28 08:20:46 -07:00 committed by GitHub
parent 916b3f6f69
commit 5fceb5a3c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 59 additions and 43 deletions

View File

@ -139,6 +139,7 @@ const MenuComponent: React.FC<Props> = props => {
style={styles.menuContentScroller} style={styles.menuContentScroller}
aria-modal={true} aria-modal={true}
accessibilityViewIsModal={true} accessibilityViewIsModal={true}
testID={`menu-content-${refocusCounter ? 'refocusing' : ''}`}
>{menuOptionComponents}</ScrollView> >{menuOptionComponents}</ScrollView>
</MenuOptions> </MenuOptions>
</Menu> </Menu>

View File

@ -1,13 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { describe, it, beforeEach } from '@jest/globals'; import { describe, it, beforeEach } from '@jest/globals';
import { act, fireEvent, render, screen, userEvent } from '@testing-library/react-native'; import { act, fireEvent, render, screen, userEvent, waitFor } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect'; import '@testing-library/jest-native/extend-expect';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import NoteScreen from './Note'; import NoteScreen from './Note';
import { MenuProvider } from 'react-native-popup-menu'; import { MenuProvider } from 'react-native-popup-menu';
import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, waitFor } from '@joplin/lib/testing/test-utils'; import { setupDatabaseAndSynchronizer, switchClient, simulateReadOnlyShareEnv, runWithFakeTimers } from '@joplin/lib/testing/test-utils';
import { waitFor as waitForWithRealTimers } from '@joplin/lib/testing/test-utils';
import Note from '@joplin/lib/models/Note'; import Note from '@joplin/lib/models/Note';
import { AppState } from '../../utils/types'; import { AppState } from '../../utils/types';
import { Store } from 'redux'; import { Store } from 'redux';
@ -61,7 +62,7 @@ const getNoteEditorControl = async () => {
}; };
const waitForNoteToMatch = async (noteId: string, note: Partial<NoteEntity>) => { const waitForNoteToMatch = async (noteId: string, note: Partial<NoteEntity>) => {
await act(() => waitFor(async () => { await act(() => waitForWithRealTimers(async () => {
const loadedNote = await Note.load(noteId); const loadedNote = await Note.load(noteId);
expect(loadedNote).toMatchObject(note); expect(loadedNote).toMatchObject(note);
})); }));
@ -72,7 +73,6 @@ const openNewNote = async (noteProperties: NoteEntity) => {
parent_id: (await Folder.defaultFolder()).id, parent_id: (await Folder.defaultFolder()).id,
...noteProperties, ...noteProperties,
}); });
const displayParentId = getDisplayParentId(note, await Folder.load(note.parent_id)); const displayParentId = getDisplayParentId(note, await Folder.load(note.parent_id));
store.dispatch({ store.dispatch({
@ -106,20 +106,31 @@ const openNoteActionsMenu = async () => {
cursor = cursor.parent; cursor = cursor.parent;
} }
// Wrap in act(...) -- this tells the test library that component state is intended to update (prevents
// warnings).
await act(async () => {
await runWithFakeTimers(async () => {
await userEvent.press(actionMenuButton); await userEvent.press(actionMenuButton);
});
// State can update until the menu content is marked as in the process of refocusing (part of the
// menu transition).
await waitFor(async () => {
expect(await screen.findByTestId('menu-content-refocusing')).toBeVisible();
});
});
}; };
const openEditor = async () => { const openEditor = async () => {
const editButton = await screen.findByLabelText('Edit'); const editButton = await screen.findByLabelText('Edit');
await userEvent.press(editButton);
fireEvent.press(editButton);
await waitFor(() => {
expect(screen.queryByLabelText('Edit')).toBeNull();
});
}; };
describe('screens/Note', () => { describe('screens/Note', () => {
beforeAll(() => {
// advanceTimers: Needed by internal note save logic
jest.useFakeTimers({ advanceTimers: true });
});
beforeEach(async () => { beforeEach(async () => {
await setupDatabaseAndSynchronizer(0); await setupDatabaseAndSynchronizer(0);
await switchClient(0); await switchClient(0);
@ -160,6 +171,8 @@ describe('screens/Note', () => {
await waitForNoteToMatch(noteId, { title: 'New title', body: 'Unchanged body' }); await waitForNoteToMatch(noteId, { title: 'New title', body: 'Unchanged body' });
// Use fake timers to allow advancing timers without pausing the test
await runWithFakeTimers(async () => {
let expectedTitle = 'New title'; let expectedTitle = 'New title';
for (let i = 0; i <= 10; i++) { for (let i = 0; i <= 10; i++) {
for (const chunk of ['!', ' test', '!!!', ' Testing']) { for (const chunk of ['!', ' test', '!!!', ' Testing']) {
@ -175,13 +188,14 @@ describe('screens/Note', () => {
await waitForNoteToMatch(noteId, { title: expectedTitle }); await waitForNoteToMatch(noteId, { title: expectedTitle });
} }
}); });
});
it('changing the note body in the editor should update the note\'s body', async () => { it('changing the note body in the editor should update the note\'s body', async () => {
const defaultBody = 'Change me!'; const defaultBody = 'Change me!';
const noteId = await openNewNote({ title: 'Unchanged title', body: defaultBody }); const noteId = await openNewNote({ title: 'Unchanged title', body: defaultBody });
const noteScreen = render(<WrappedNoteScreen />); const noteScreen = render(<WrappedNoteScreen />);
await act(async () => await runWithFakeTimers(async () => {
await openEditor(); await openEditor();
const editor = await getNoteEditorControl(); const editor = await getNoteEditorControl();
editor.select(defaultBody.length, defaultBody.length); editor.select(defaultBody.length, defaultBody.length);
@ -197,11 +211,11 @@ describe('screens/Note', () => {
// TODO: Decreasing this below 100 causes the test to fail. // TODO: Decreasing this below 100 causes the test to fail.
// See issue #11125. // See issue #11125.
await jest.advanceTimersByTimeAsync(150); await jest.advanceTimersByTimeAsync(450);
noteScreen.unmount(); noteScreen.unmount();
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test. Test!' }); await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test. Test!' });
}));
}); });
it('pressing "delete" should move the note to the trash', async () => { it('pressing "delete" should move the note to the trash', async () => {
@ -212,9 +226,9 @@ describe('screens/Note', () => {
const deleteButton = await screen.findByText('Delete'); const deleteButton = await screen.findByText('Delete');
fireEvent.press(deleteButton); fireEvent.press(deleteButton);
await act(() => waitFor(async () => { await waitFor(async () => {
expect((await Note.load(noteId)).deleted_time).toBeGreaterThan(0); expect((await Note.load(noteId)).deleted_time).toBeGreaterThan(0);
})); });
}); });
it('pressing "delete permanently" should permanently delete a note', async () => { it('pressing "delete permanently" should permanently delete a note', async () => {
@ -229,9 +243,9 @@ describe('screens/Note', () => {
const deleteButton = await screen.findByText('Permanently delete note'); const deleteButton = await screen.findByText('Permanently delete note');
fireEvent.press(deleteButton); fireEvent.press(deleteButton);
await act(() => waitFor(async () => { await waitFor(async () => {
expect(await Note.load(noteId)).toBeUndefined(); expect(await Note.load(noteId)).toBeUndefined();
})); });
expect(shim.showMessageBox).toHaveBeenCalled(); expect(shim.showMessageBox).toHaveBeenCalled();
}); });

View File

@ -1125,7 +1125,8 @@ export const runWithFakeTimers = async (callback: ()=> Promise<void>) => {
throw new Error('Fake timers are only supported in jest.'); throw new Error('Fake timers are only supported in jest.');
} }
jest.useFakeTimers(); // advanceTimers: Needed by Joplin's database driver
jest.useFakeTimers({ advanceTimers: true });
// The shim.setTimeout and similar functions need to be changed to // The shim.setTimeout and similar functions need to be changed to
// use fake timers. // use fake timers.