1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +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}
aria-modal={true}
accessibilityViewIsModal={true}
testID={`menu-content-${refocusCounter ? 'refocusing' : ''}`}
>{menuOptionComponents}</ScrollView>
</MenuOptions>
</Menu>

View File

@ -1,13 +1,14 @@
import * as React from 'react';
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 { Provider } from 'react-redux';
import NoteScreen from './Note';
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 { AppState } from '../../utils/types';
import { Store } from 'redux';
@ -61,7 +62,7 @@ const getNoteEditorControl = async () => {
};
const waitForNoteToMatch = async (noteId: string, note: Partial<NoteEntity>) => {
await act(() => waitFor(async () => {
await act(() => waitForWithRealTimers(async () => {
const loadedNote = await Note.load(noteId);
expect(loadedNote).toMatchObject(note);
}));
@ -72,7 +73,6 @@ const openNewNote = async (noteProperties: NoteEntity) => {
parent_id: (await Folder.defaultFolder()).id,
...noteProperties,
});
const displayParentId = getDisplayParentId(note, await Folder.load(note.parent_id));
store.dispatch({
@ -106,20 +106,31 @@ const openNoteActionsMenu = async () => {
cursor = cursor.parent;
}
await userEvent.press(actionMenuButton);
// 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);
});
// 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 editButton = await screen.findByLabelText('Edit');
await userEvent.press(editButton);
fireEvent.press(editButton);
await waitFor(() => {
expect(screen.queryByLabelText('Edit')).toBeNull();
});
};
describe('screens/Note', () => {
beforeAll(() => {
// advanceTimers: Needed by internal note save logic
jest.useFakeTimers({ advanceTimers: true });
});
beforeEach(async () => {
await setupDatabaseAndSynchronizer(0);
await switchClient(0);
@ -160,20 +171,23 @@ describe('screens/Note', () => {
await waitForNoteToMatch(noteId, { title: 'New title', body: 'Unchanged body' });
let expectedTitle = 'New title';
for (let i = 0; i <= 10; i++) {
for (const chunk of ['!', ' test', '!!!', ' Testing']) {
jest.advanceTimersByTime(i % 5);
await user.type(titleInput, chunk);
expectedTitle += chunk;
// Use fake timers to allow advancing timers without pausing the test
await runWithFakeTimers(async () => {
let expectedTitle = 'New title';
for (let i = 0; i <= 10; i++) {
for (const chunk of ['!', ' test', '!!!', ' Testing']) {
jest.advanceTimersByTime(i % 5);
await user.type(titleInput, chunk);
expectedTitle += chunk;
// Don't verify after each input event -- this allows the save action queue to fill.
if (i % 4 === 0) {
await waitForNoteToMatch(noteId, { title: expectedTitle });
// Don't verify after each input event -- this allows the save action queue to fill.
if (i % 4 === 0) {
await waitForNoteToMatch(noteId, { title: expectedTitle });
}
}
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 () => {
@ -181,27 +195,27 @@ describe('screens/Note', () => {
const noteId = await openNewNote({ title: 'Unchanged title', body: defaultBody });
const noteScreen = render(<WrappedNoteScreen />);
await act(async () => await runWithFakeTimers(async () => {
await openEditor();
const editor = await getNoteEditorControl();
editor.select(defaultBody.length, defaultBody.length);
await openEditor();
const editor = await getNoteEditorControl();
editor.select(defaultBody.length, defaultBody.length);
editor.insertText(' Testing!!!');
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!!' });
editor.insertText(' Testing!!!');
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!!' });
editor.insertText(' This is a test.');
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test.' });
editor.insertText(' This is a test.');
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test.' });
// should also save changes made shortly before unmounting
editor.insertText(' Test!');
// should also save changes made shortly before unmounting
editor.insertText(' Test!');
// TODO: Decreasing this below 100 causes the test to fail.
// See issue #11125.
await jest.advanceTimersByTimeAsync(150);
noteScreen.unmount();
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test. Test!' });
// TODO: Decreasing this below 100 causes the test to fail.
// See issue #11125.
await jest.advanceTimersByTimeAsync(450);
noteScreen.unmount();
await waitForNoteToMatch(noteId, { body: 'Change me! Testing!!! This is a test. Test!' });
}));
});
it('pressing "delete" should move the note to the trash', async () => {
@ -212,9 +226,9 @@ describe('screens/Note', () => {
const deleteButton = await screen.findByText('Delete');
fireEvent.press(deleteButton);
await act(() => waitFor(async () => {
await waitFor(async () => {
expect((await Note.load(noteId)).deleted_time).toBeGreaterThan(0);
}));
});
});
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');
fireEvent.press(deleteButton);
await act(() => waitFor(async () => {
await waitFor(async () => {
expect(await Note.load(noteId)).toBeUndefined();
}));
});
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.');
}
jest.useFakeTimers();
// advanceTimers: Needed by Joplin's database driver
jest.useFakeTimers({ advanceTimers: true });
// The shim.setTimeout and similar functions need to be changed to
// use fake timers.