Skip to content

Commit 38f7822

Browse files
authored
Merge pull request Expensify#66688 from nkdengineer/fix/65667
Room - After creating room and changing priority mode offline, room shows skeleton
2 parents 602255d + 918fcb8 commit 38f7822

3 files changed

Lines changed: 291 additions & 2 deletions

File tree

src/components/ReportActionsSkeletonView/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function ReportActionsSkeletonView({shouldAnimate = true, possibleVisibleContent
4545
);
4646
}
4747
}
48-
return <View>{skeletonViewLines}</View>;
48+
return <View testID="ReportActionsSkeletonView">{skeletonViewLines}</View>;
4949
}
5050

5151
ReportActionsSkeletonView.displayName = 'ReportActionsSkeletonView';

src/pages/home/report/ReportActionsView.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,13 @@ function ReportActionsView({
276276
};
277277
}, [isTheFirstReportActionIsLinked]);
278278

279-
if ((isLoadingInitialReportActions && (isReportDataIncomplete || isMissingReportActions) && !isOffline) ?? isLoadingApp) {
279+
// Show skeleton while loading initial report actions when data is incomplete/missing and online
280+
const shouldShowSkeletonForInitialLoad = isLoadingInitialReportActions && (isReportDataIncomplete || isMissingReportActions) && !isOffline;
281+
282+
// Show skeleton while the app is loading and we're online
283+
const shouldShowSkeletonForAppLoad = isLoadingApp && !isOffline;
284+
285+
if (shouldShowSkeletonForInitialLoad ?? shouldShowSkeletonForAppLoad) {
280286
return <ReportActionsSkeletonView />;
281287
}
282288

tests/ui/ReportActionsViewTest.tsx

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import type * as ReactNavigation from '@react-navigation/native';
2+
import {render, screen} from '@testing-library/react-native';
3+
import React from 'react';
4+
import Onyx from 'react-native-onyx';
5+
import type {OnyxEntry} from 'react-native-onyx';
6+
import useNetwork from '@hooks/useNetwork';
7+
import useOnyx from '@hooks/useOnyx';
8+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
9+
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
10+
import ReportActionsView from '@pages/home/report/ReportActionsView';
11+
import CONST from '@src/CONST';
12+
import ONYXKEYS from '@src/ONYXKEYS';
13+
import type * as OnyxTypes from '@src/types/onyx';
14+
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
15+
16+
const mockUseIsFocused = jest.fn().mockReturnValue(false);
17+
jest.mock('@react-navigation/native', () => {
18+
const actualNav = jest.requireActual<typeof ReactNavigation>('@react-navigation/native');
19+
return {
20+
...actualNav,
21+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
22+
useIsFocused: () => mockUseIsFocused(),
23+
useRoute: jest.fn(),
24+
useNavigationState: jest.fn(),
25+
createNavigationContainerRef: () => ({
26+
getState: () => jest.fn(),
27+
}),
28+
};
29+
});
30+
31+
jest.mock('@hooks/useNetwork', () => jest.fn());
32+
jest.mock('@hooks/useOnyx', () => jest.fn());
33+
jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
34+
jest.mock('@hooks/useTransactionsAndViolationsForReport', () => jest.fn());
35+
36+
const mockUseNetwork = useNetwork as jest.MockedFunction<typeof useNetwork>;
37+
const mockUseOnyx = useOnyx as jest.MockedFunction<typeof useOnyx>;
38+
const mockUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction<typeof useResponsiveLayout>;
39+
const mockUseTransactionsAndViolationsForReport = useTransactionsAndViolationsForReport as jest.MockedFunction<typeof useTransactionsAndViolationsForReport>;
40+
jest.mock('@hooks/useCopySelectionHelper', () => jest.fn());
41+
jest.mock('@hooks/useLoadReportActions', () =>
42+
jest.fn(() => ({
43+
loadOlderChats: jest.fn(),
44+
loadNewerChats: jest.fn(),
45+
})),
46+
);
47+
jest.mock('@hooks/usePrevious', () => jest.fn());
48+
49+
jest.mock('@pages/home/report/ReportActionsList', () =>
50+
jest.fn(({sortedReportActions}: {sortedReportActions: OnyxTypes.ReportAction[]}) => {
51+
if (sortedReportActions && sortedReportActions.length > 0) {
52+
return null; // Simulate normal content
53+
}
54+
return null;
55+
}),
56+
);
57+
jest.mock('@pages/home/report/UserTypingEventListener', () => jest.fn(() => null));
58+
59+
jest.mock('@libs/actions/Report', () => ({
60+
updateLoadingInitialReportAction: jest.fn(),
61+
}));
62+
63+
// Mock report data
64+
const mockReport: OnyxTypes.Report = {
65+
reportID: '123',
66+
reportName: 'Test Report',
67+
chatReportID: '456',
68+
ownerAccountID: 123,
69+
lastVisibleActionCreated: '2023-01-01',
70+
total: 0,
71+
};
72+
73+
const mockReportActions: OnyxTypes.ReportAction[] = [
74+
{
75+
reportActionID: '1',
76+
actionName: CONST.REPORT.ACTIONS.TYPE.CREATED,
77+
created: '2023-01-01',
78+
actorAccountID: 123,
79+
message: [{type: 'COMMENT', html: 'Test message', text: 'Test message'}],
80+
originalMessage: {},
81+
shouldShow: true,
82+
person: [{type: 'TEXT', style: 'strong', text: 'Test User'}],
83+
pendingAction: null,
84+
errors: {},
85+
},
86+
{
87+
reportActionID: '2',
88+
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
89+
created: '2023-01-02',
90+
actorAccountID: 124,
91+
message: [{type: 'COMMENT', html: 'Another message', text: 'Another message'}],
92+
originalMessage: {},
93+
shouldShow: true,
94+
person: [{type: 'TEXT', style: 'strong', text: 'Another User'}],
95+
pendingAction: null,
96+
errors: {},
97+
},
98+
];
99+
100+
const renderReportActionsView = (
101+
props: Partial<{
102+
report: OnyxTypes.Report;
103+
reportActions?: OnyxTypes.ReportAction[];
104+
parentReportAction: OnyxEntry<OnyxTypes.ReportAction>;
105+
isLoadingInitialReportActions?: boolean;
106+
transactionThreadReportID?: string | null;
107+
hasNewerActions: boolean;
108+
hasOlderActions: boolean;
109+
}> = {},
110+
) => {
111+
const defaultProps = {
112+
report: mockReport,
113+
reportActions: mockReportActions,
114+
parentReportAction: null as unknown as OnyxEntry<OnyxTypes.ReportAction>,
115+
isLoadingInitialReportActions: false,
116+
hasNewerActions: false,
117+
hasOlderActions: false,
118+
...props,
119+
};
120+
121+
// eslint-disable-next-line react/jsx-props-no-spreading
122+
return render(<ReportActionsView {...defaultProps} />);
123+
};
124+
125+
describe('ReportActionsView', () => {
126+
beforeAll(() => {
127+
Onyx.init({
128+
keys: ONYXKEYS,
129+
});
130+
});
131+
132+
beforeEach(() => {
133+
jest.clearAllMocks();
134+
135+
mockUseResponsiveLayout.mockReturnValue({
136+
shouldUseNarrowLayout: false,
137+
isSmallScreenWidth: false,
138+
isInNarrowPaneModal: false,
139+
isExtraSmallScreenHeight: false,
140+
isMediumScreenWidth: false,
141+
isLargeScreenWidth: true,
142+
isExtraLargeScreenWidth: false,
143+
isExtraSmallScreenWidth: false,
144+
isSmallScreen: false,
145+
onboardingIsMediumOrLargerScreenWidth: true,
146+
});
147+
148+
mockUseTransactionsAndViolationsForReport.mockReturnValue({
149+
transactions: {},
150+
violations: {},
151+
});
152+
153+
mockUseOnyx.mockImplementation((key: string) => {
154+
if (key === ONYXKEYS.IS_LOADING_APP) {
155+
return [false, {status: 'loaded'}];
156+
}
157+
if (key === ONYXKEYS.ARE_TRANSLATIONS_LOADING) {
158+
return [false, {status: 'loaded'}];
159+
}
160+
if (key.includes('reportActions')) {
161+
return [[], {status: 'loaded'}];
162+
}
163+
if (key.includes('report')) {
164+
return [undefined, {status: 'loaded'}];
165+
}
166+
return [undefined, {status: 'loaded'}];
167+
});
168+
});
169+
170+
afterEach(async () => {
171+
await waitForBatchedUpdatesWithAct();
172+
await Onyx.clear();
173+
});
174+
175+
describe('Skeleton Loading States', () => {
176+
it('should show skeleton when shouldShowSkeletonForAppLoad is true (isLoadingApp is true and isOffline is false)', () => {
177+
mockUseNetwork.mockReturnValue({
178+
isOffline: false,
179+
});
180+
181+
mockUseOnyx.mockImplementation((key: string) => {
182+
if (key === ONYXKEYS.IS_LOADING_APP) {
183+
return [true, {status: 'loaded'}];
184+
}
185+
if (key === ONYXKEYS.ARE_TRANSLATIONS_LOADING) {
186+
return [false, {status: 'loaded'}];
187+
}
188+
if (key.includes('reportActions')) {
189+
return [[], {status: 'loaded'}];
190+
}
191+
if (key.includes('report')) {
192+
return [undefined, {status: 'loaded'}];
193+
}
194+
return [undefined, {status: 'loaded'}];
195+
});
196+
197+
// Empty report actions to trigger isMissingReportActions condition
198+
renderReportActionsView({
199+
reportActions: [],
200+
});
201+
202+
expect(screen.getByTestId('ReportActionsSkeletonView')).toBeTruthy();
203+
});
204+
205+
it('should not show skeleton when shouldShowSkeletonForAppLoad is false (isLoadingApp is false and isOffline is false)', () => {
206+
mockUseNetwork.mockReturnValue({
207+
isOffline: false,
208+
});
209+
210+
mockUseOnyx.mockImplementation((key: string) => {
211+
if (key === ONYXKEYS.IS_LOADING_APP) {
212+
return [false, {status: 'loaded'}];
213+
}
214+
if (key === ONYXKEYS.ARE_TRANSLATIONS_LOADING) {
215+
return [false, {status: 'loaded'}];
216+
}
217+
if (key.includes('reportActions')) {
218+
return [[], {status: 'loaded'}];
219+
}
220+
if (key.includes('report')) {
221+
return [undefined, {status: 'loaded'}];
222+
}
223+
return [undefined, {status: 'loaded'}];
224+
});
225+
226+
renderReportActionsView();
227+
228+
expect(screen.queryByTestId('ReportActionsSkeletonView')).toBeNull();
229+
});
230+
231+
it('should not show skeleton when shouldShowSkeletonForAppLoad is false (isLoadingApp is true and isOffline is true)', () => {
232+
mockUseNetwork.mockReturnValue({
233+
isOffline: true,
234+
});
235+
236+
mockUseOnyx.mockImplementation((key: string) => {
237+
if (key === ONYXKEYS.IS_LOADING_APP) {
238+
return [true, {status: 'loaded'}];
239+
}
240+
if (key === ONYXKEYS.ARE_TRANSLATIONS_LOADING) {
241+
return [false, {status: 'loaded'}];
242+
}
243+
if (key.includes('reportActions')) {
244+
return [[], {status: 'loaded'}];
245+
}
246+
if (key.includes('report')) {
247+
return [undefined, {status: 'loaded'}];
248+
}
249+
return [undefined, {status: 'loaded'}];
250+
});
251+
252+
renderReportActionsView();
253+
254+
expect(screen.queryByTestId('ReportActionsSkeletonView')).toBeNull();
255+
});
256+
257+
it('should not show skeleton when both isLoadingApp is false and isOffline is true', () => {
258+
mockUseNetwork.mockReturnValue({
259+
isOffline: true,
260+
});
261+
262+
mockUseOnyx.mockImplementation((key: string) => {
263+
if (key === ONYXKEYS.IS_LOADING_APP) {
264+
return [false, {status: 'loaded'}];
265+
}
266+
if (key === ONYXKEYS.ARE_TRANSLATIONS_LOADING) {
267+
return [false, {status: 'loaded'}];
268+
}
269+
if (key.includes('reportActions')) {
270+
return [[], {status: 'loaded'}];
271+
}
272+
if (key.includes('report')) {
273+
return [undefined, {status: 'loaded'}];
274+
}
275+
return [undefined, {status: 'loaded'}];
276+
});
277+
278+
renderReportActionsView();
279+
280+
expect(screen.queryByTestId('ReportActionsSkeletonView')).toBeNull();
281+
});
282+
});
283+
});

0 commit comments

Comments
 (0)