Add performance metrics to the app (#7953)

* Add performance metrics to the app

* add batcher and improve handling

* Add tests

* Fix test

* Address feedback

* Address feedback

* Address feedback

* update podfile
This commit is contained in:
Daniel Espino García
2024-06-12 09:45:27 +02:00
committed by GitHub
parent daa38c5fd1
commit 5f01f9e9af
32 changed files with 1383 additions and 28 deletions

View File

@@ -2,11 +2,12 @@
// See LICENSE.txt for license information.
import {DatabaseProvider} from '@nozbe/watermelondb/react';
import {render} from '@testing-library/react-native';
import {render, type RenderOptions} from '@testing-library/react-native';
import React, {type ReactElement} from 'react';
import {IntlProvider} from 'react-intl';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import ServerUrlProvider from '@context/server';
import {ThemeContext, getDefaultThemeByAppearance} from '@context/theme';
import {getTranslations} from '@i18n';
@@ -48,13 +49,13 @@ export function renderWithIntlAndTheme(ui: ReactElement, {locale = 'en', ...rend
return render(ui, {wrapper: Wrapper, ...renderOptions});
}
export function renderWithEverything(ui: ReactElement, {locale = 'en', database, ...renderOptions}: {locale?: string; database?: Database; renderOptions?: any} = {}) {
export function renderWithEverything(ui: ReactElement, {locale = 'en', database, serverUrl, ...renderOptions}: {locale?: string; database?: Database; serverUrl?: string; renderOptions?: RenderOptions} = {}) {
function Wrapper({children}: {children: ReactElement}) {
if (!database) {
return null;
}
return (
const wrapper = (
<DatabaseProvider database={database}>
<IntlProvider
locale={locale}
@@ -68,6 +69,16 @@ export function renderWithEverything(ui: ReactElement, {locale = 'en', database,
</IntlProvider>
</DatabaseProvider>
);
if (serverUrl) {
return (
<ServerUrlProvider server={{displayName: serverUrl, url: serverUrl}}>
{wrapper}
</ServerUrlProvider>
);
}
return wrapper;
}
return render(ui, {wrapper: Wrapper, ...renderOptions});

11
test/mock_api_client.ts Normal file
View File

@@ -0,0 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {RequestOptions} from '@mattermost/react-native-network-client';
export const mockApiClient = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
get: jest.fn((url: string, options?: RequestOptions) => ({status: 200, ok: true})),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
post: jest.fn((url: string, options?: RequestOptions) => ({status: 200, ok: true})),
};

View File

@@ -8,6 +8,9 @@ import * as ReactNative from 'react-native';
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
import {v4 as uuidv4} from 'uuid';
import {mockApiClient} from './mock_api_client';
import type {RequestOptions} from '@mattermost/react-native-network-client';
import type {ReadDirItem, StatResult} from 'react-native-fs';
import 'react-native-gesture-handler/jestSetup';
@@ -186,17 +189,11 @@ jest.doMock('react-native', () => {
jest.mock('react-native-vector-icons', () => {
const React = jest.requireActual('react');
const PropTypes = jest.requireActual('prop-types');
class CompassIcon extends React.PureComponent {
render() {
return React.createElement('Icon', this.props);
}
}
CompassIcon.propTypes = {
name: PropTypes.string,
size: PropTypes.number,
style: PropTypes.oneOfType([PropTypes.array, PropTypes.number, PropTypes.object]),
};
CompassIcon.getImageSource = jest.fn().mockResolvedValue({});
return {
createIconSet: () => CompassIcon,
@@ -256,6 +253,8 @@ jest.mock('react-native-device-info', () => {
hasNotch: jest.fn(() => true),
isTablet: jest.fn(() => false),
getApplicationName: jest.fn(() => 'Mattermost'),
getSystemName: jest.fn(() => 'ios'),
getSystemVersion: jest.fn(() => '0.0.0'),
};
});
@@ -367,6 +366,8 @@ jest.mock('@screens/navigation', () => ({
popToRoot: jest.fn(() => Promise.resolve()),
dismissModal: jest.fn(() => Promise.resolve()),
dismissAllModals: jest.fn(() => Promise.resolve()),
dismissAllModalsAndPopToScreen: jest.fn(),
dismissAllModalsAndPopToRoot: jest.fn(),
dismissOverlay: jest.fn(() => Promise.resolve()),
}));
@@ -386,6 +387,22 @@ jest.mock('@mattermost/react-native-emm', () => ({
useManagedConfig: () => ({}),
}));
jest.mock('@react-native-clipboard/clipboard', () => ({}));
jest.mock('react-native-document-picker', () => ({}));
jest.mock('@mattermost/react-native-network-client', () => ({
getOrCreateAPIClient: (serverUrl: string) => ({client: {
baseUrl: serverUrl,
get: (url: string, options?: RequestOptions) => mockApiClient.get(`${serverUrl}${url}`, options),
post: (url: string, options?: RequestOptions) => mockApiClient.post(`${serverUrl}${url}`, options),
invalidate: jest.fn(),
}}),
RetryTypes: {
EXPONENTIAL_RETRY: 'exponential',
},
}));
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
@@ -399,7 +416,15 @@ jest.mock('react-native-haptic-feedback', () => {
};
});
declare const global: {requestAnimationFrame: (callback: any) => void};
declare const global: {
requestAnimationFrame: (callback: () => void) => void;
performance: {
now: () => number;
};
};
global.requestAnimationFrame = (callback) => {
setTimeout(callback, 0);
};
global.performance.now = () => Date.now();

View File

@@ -10,6 +10,7 @@ import nock from 'nock';
import Config from '@assets/config.json';
import {Client} from '@client/rest';
import {ActionType} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
@@ -18,7 +19,6 @@ import {generateId} from '@utils/general';
import type {APIClientInterface} from '@mattermost/react-native-network-client';
const PASSWORD = 'password1';
const DEFAULT_LOCALE = 'en';
class TestHelper {
@@ -51,8 +51,8 @@ class TestHelper {
this.basicRoles = null;
}
setupServerDatabase = async () => {
const serverUrl = 'https://appv1.mattermost.com';
setupServerDatabase = async (url?: string) => {
const serverUrl = url || 'https://appv1.mattermost.com';
await DatabaseManager.init([serverUrl]);
const {database, operator} = DatabaseManager.serverDatabases[serverUrl]!;
@@ -112,6 +112,13 @@ class TestHelper {
systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}],
});
await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_NEW,
order: [this.basicPost!.id],
posts: [this.basicPost!],
prepareRecordsOnly: false,
});
return {database, operator};
};
@@ -291,7 +298,7 @@ class TestHelper {
return 'success' + this.generateId() + '@simulator.amazonses.com';
};
fakePost = (channelId: string) => {
fakePost = (channelId: string, userId?: string): Post => {
const time = Date.now();
return {
@@ -301,6 +308,17 @@ class TestHelper {
update_at: time,
message: `Unit Test ${this.generateId()}`,
type: '',
delete_at: 0,
edit_at: 0,
hashtags: '',
is_pinned: false,
metadata: {},
original_id: '',
pending_post_id: '',
props: {},
reply_count: 0,
root_id: '',
user_id: userId || this.generateId(),
};
};
@@ -314,7 +332,7 @@ class TestHelper {
};
};
fakeTeam = () => {
fakeTeam = (): Team => {
const name = this.generateId();
let inviteId = this.generateId();
if (inviteId.length > 32) {
@@ -322,6 +340,7 @@ class TestHelper {
}
return {
id: this.generateId(),
name,
display_name: `Unit Test ${name}`,
type: 'O' as const,
@@ -334,6 +353,9 @@ class TestHelper {
allow_open_invite: true,
group_constrained: false,
last_team_icon_update: 0,
create_at: 0,
delete_at: 0,
update_at: 0,
};
};
@@ -361,11 +383,9 @@ class TestHelper {
};
};
fakeUser = () => {
fakeUser = (): UserProfile => {
return {
email: this.fakeEmail(),
allow_marketing: true,
password: PASSWORD,
locale: DEFAULT_LOCALE,
username: this.generateId(),
first_name: this.generateId(),
@@ -373,6 +393,27 @@ class TestHelper {
create_at: Date.now(),
delete_at: 0,
roles: 'system_user',
auth_service: '',
id: this.generateId(),
nickname: '',
notify_props: this.fakeNotifyProps(),
position: '',
update_at: 0,
};
};
fakeNotifyProps = (): UserNotifyProps => {
return {
channel: 'false',
comments: 'root',
desktop: 'default',
desktop_sound: 'false',
email: 'false',
first_name: 'false',
highlight_keys: '',
mention_keys: '',
push: 'default',
push_status: 'away',
};
};
@@ -665,6 +706,7 @@ class TestHelper {
};
wait = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
tick = () => new Promise((r) => setImmediate(r));
}
export default new TestHelper();