Skip to content

Commit a6c863a

Browse files
fix: engagement dashboard becoming unresponsive when clicking "See on Engagement Dashboard" (RocketChat#40058)
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com> Co-authored-by: Douglas Fabris <devfabris@gmail.com>
1 parent 85f001c commit a6c863a

File tree

4 files changed

+158
-2
lines changed

4 files changed

+158
-2
lines changed

.changeset/cool-parents-buy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
'@rocket.chat/mock-providers': patch
4+
---
5+
6+
Fixed UI becoming unresponsive after clicking "See on Engagement Dashboard" from the workspace info card, which required a manual page refresh to recover.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import type { RouterContextValue } from '@rocket.chat/ui-contexts';
3+
import { render } from '@testing-library/react';
4+
5+
import EngagementDashboardRoute from './EngagementDashboardRoute';
6+
import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks';
7+
import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule';
8+
9+
jest.mock('../../../hooks/useHasLicenseModule', () => ({ useHasLicenseModule: jest.fn() }));
10+
jest.mock('../../../components/GenericUpsellModal/hooks', () => ({ useUpsellActions: jest.fn() }));
11+
jest.mock('./EngagementDashboardPage', () => () => null);
12+
jest.mock('../../../components/PageSkeleton', () => () => null);
13+
jest.mock('../../notAuthorized/NotAuthorizedPage', () => () => null);
14+
jest.mock('../../../../app/utils/client/getURL', () => ({ getURL: (path: string) => path }));
15+
16+
const buildWrapper = (router: Partial<RouterContextValue>, tab?: string) => {
17+
const builder = mockAppRoot()
18+
.withPermission('view-engagement-dashboard')
19+
.withEndpoint('POST', '/v1/statistics.telemetry', () => ({}))
20+
.withRouter(router);
21+
22+
if (tab) {
23+
builder.withRouteParameter('tab', tab);
24+
}
25+
26+
return builder.build();
27+
};
28+
29+
const createSubscribeTracker = () => {
30+
const unsubscribes: jest.Mock[] = [];
31+
32+
const subscribeToRouteChange = jest.fn((_cb: () => void) => {
33+
const unsub = jest.fn();
34+
unsubscribes.push(unsub);
35+
return unsub;
36+
});
37+
38+
const allUnsubscribed = () => unsubscribes.length > 0 && unsubscribes.every((u) => u.mock.calls.length > 0);
39+
40+
const noneUnsubscribed = () => unsubscribes.every((u) => u.mock.calls.length === 0);
41+
42+
const getLastRouteChangeCallback = () => {
43+
const callback = subscribeToRouteChange.mock.calls.at(-1)?.[0];
44+
expect(callback).toBeDefined();
45+
return callback as () => void;
46+
};
47+
48+
return { subscribeToRouteChange, allUnsubscribed, noneUnsubscribed, getLastRouteChangeCallback };
49+
};
50+
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
54+
jest.mocked(useHasLicenseModule).mockReturnValue({
55+
isPending: false,
56+
data: true,
57+
} as unknown as ReturnType<typeof useHasLicenseModule>);
58+
59+
jest.mocked(useUpsellActions).mockReturnValue({
60+
shouldShowUpsell: false,
61+
cloudWorkspaceHadTrial: false,
62+
handleManageSubscription: jest.fn(),
63+
handleTalkToSales: jest.fn(),
64+
});
65+
});
66+
67+
describe('EngagementDashboardRoute', () => {
68+
it('subscribes to route changes on mount', () => {
69+
const { subscribeToRouteChange } = createSubscribeTracker();
70+
71+
render(<EngagementDashboardRoute />, {
72+
wrapper: buildWrapper({ subscribeToRouteChange }, 'users'),
73+
});
74+
75+
expect(subscribeToRouteChange).toHaveBeenCalled();
76+
});
77+
78+
it('unsubscribes all listeners on unmount', () => {
79+
const { subscribeToRouteChange, allUnsubscribed } = createSubscribeTracker();
80+
81+
const { unmount } = render(<EngagementDashboardRoute />, {
82+
wrapper: buildWrapper({ subscribeToRouteChange }, 'users'),
83+
});
84+
85+
unmount();
86+
expect(allUnsubscribed()).toBe(true);
87+
});
88+
89+
it('does not redirect when the tab is valid', () => {
90+
const navigate = jest.fn();
91+
const { subscribeToRouteChange, getLastRouteChangeCallback } = createSubscribeTracker();
92+
93+
render(<EngagementDashboardRoute />, {
94+
wrapper: buildWrapper({ subscribeToRouteChange, navigate }, 'users'),
95+
});
96+
97+
const routeChangeCallback = getLastRouteChangeCallback();
98+
routeChangeCallback();
99+
100+
expect(navigate).not.toHaveBeenCalled();
101+
});
102+
103+
it('redirects to the users tab when the tab is invalid', () => {
104+
const navigate = jest.fn();
105+
const { subscribeToRouteChange, getLastRouteChangeCallback } = createSubscribeTracker();
106+
107+
render(<EngagementDashboardRoute />, {
108+
wrapper: buildWrapper({ subscribeToRouteChange, navigate }),
109+
});
110+
111+
const routeChangeCallback = getLastRouteChangeCallback();
112+
routeChangeCallback();
113+
114+
expect(navigate).toHaveBeenCalledWith({ pattern: '/admin/engagement/:tab?', params: { tab: 'users' } }, { replace: true });
115+
});
116+
117+
it('cleans up all subscriptions on unmount and resubscribes on remount', () => {
118+
const first = createSubscribeTracker();
119+
120+
const { unmount } = render(<EngagementDashboardRoute />, {
121+
wrapper: buildWrapper({ subscribeToRouteChange: first.subscribeToRouteChange }, 'users'),
122+
});
123+
124+
expect(first.subscribeToRouteChange).toHaveBeenCalled();
125+
unmount();
126+
expect(first.allUnsubscribed()).toBe(true);
127+
128+
const second = createSubscribeTracker();
129+
130+
render(<EngagementDashboardRoute />, {
131+
wrapper: buildWrapper({ subscribeToRouteChange: second.subscribeToRouteChange }, 'messages'),
132+
});
133+
134+
expect(second.subscribeToRouteChange).toHaveBeenCalled();
135+
expect(second.noneUnsubscribed()).toBe(true);
136+
});
137+
});

apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardRoute.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ const EngagementDashboardRoute = (): ReactElement | null => {
5050
/>,
5151
);
5252
}
53+
}, [shouldShowUpsell, setModal, t, handleManageSubscription]);
5354

54-
router.subscribeToRouteChange(() => {
55+
useEffect(() => {
56+
return router.subscribeToRouteChange(() => {
5557
if (!isValidTab(tab)) {
5658
router.navigate(
5759
{
@@ -62,7 +64,7 @@ const EngagementDashboardRoute = (): ReactElement | null => {
6264
);
6365
}
6466
});
65-
}, [shouldShowUpsell, router, tab, setModal, t, handleManageSubscription]);
67+
}, [router, tab]);
6668

6769
if (isModalOpen || isPending) {
6870
return <PageSkeleton />;

packages/mock-providers/src/MockedAppRootBuilder.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,17 @@ export class MockedAppRootBuilder {
474474
return this;
475475
}
476476

477+
withRouter(overrides: Partial<ContextType<typeof RouterContext>>): this {
478+
this.router = { ...this.router, ...overrides };
479+
return this;
480+
}
481+
482+
withRouteParameter(name: string, value: string): this {
483+
const innerFn = this.router.getRouteParameters;
484+
this.router.getRouteParameters = () => ({ ...innerFn(), [name]: value });
485+
return this;
486+
}
487+
477488
withRole(role: string): this {
478489
if (!this.user.user) {
479490
throw new Error('user is not defined');

0 commit comments

Comments
 (0)