Skip to content

Commit 7b7e0c2

Browse files
authored
Add status when simulator disconnects (#3582)
* feat: add disconnection banner and handle online/offline events in simulator * fix: correct disconnection banner logic in simulator component * fix: update simulator connection tests for accurate banner display * codacy fixes * fix: adjust font size for error message and enhance simulator offline test * fix: enhance disconnection banner tests to verify restoration of connection
1 parent f44562b commit 7b7e0c2

File tree

3 files changed

+146
-6
lines changed

3 files changed

+146
-6
lines changed

src/components/simulator/Simulator.module.css

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@
237237

238238
.Simulator .Screen .Controls div {
239239
border: none;
240-
241240
border-radius: 999px;
242241
padding: 5px;
243242
padding-left: 10px;
@@ -247,7 +246,6 @@
247246
background-color: white;
248247
display: flex;
249248
box-shadow: 0 1px silver;
250-
251249
line-height: 5px;
252250
}
253251

@@ -367,3 +365,14 @@
367365
white-space: pre-wrap;
368366
font-weight: 600;
369367
}
368+
369+
.DisconnectedBanner {
370+
margin: 8px 12px;
371+
padding: 4px 6px;
372+
border-radius: 7px;
373+
background-color: #fdd;
374+
color: #a94442;
375+
font-size: 0.75rem;
376+
font-weight: bold;
377+
text-align: center;
378+
}

src/components/simulator/Simulator.test.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,68 @@ test('simulator should reset on clicking the reset button message', async () =>
232232
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
233233
});
234234
});
235+
236+
test('disconnection banner should not be displayed when simulator is connected', async () => {
237+
const connectionProps = getDefaultProps();
238+
connectionProps.showSimulator = true;
239+
240+
Object.defineProperty(window.navigator, 'onLine', {
241+
writable: true,
242+
value: true,
243+
});
244+
245+
const { queryByText } = render(
246+
<MockedProvider mocks={mocks}>
247+
<Simulator {...connectionProps} />
248+
</MockedProvider>
249+
);
250+
251+
await waitFor(() => {
252+
expect(queryByText('Simulator connection lost. Try to reload.')).not.toBeInTheDocument();
253+
});
254+
});
255+
256+
test('disconnection banner should be displayed when simulator connection is lost', async () => {
257+
const disconnectionProps = getDefaultProps();
258+
disconnectionProps.showSimulator = true;
259+
mockedAxios.post.mockImplementation(() => Promise.resolve({ data: {} }));
260+
const { getByTestId, getByText, queryByText } = render(
261+
<MockedProvider mocks={mocks}>
262+
<Simulator {...disconnectionProps} />
263+
</MockedProvider>
264+
);
265+
266+
await waitFor(() => {
267+
expect(getByTestId('simulatorInput')).toBeInTheDocument();
268+
});
269+
270+
fireEvent.change(getByTestId('simulatorInput'), { target: { value: 'something' } });
271+
272+
await waitFor(() => {
273+
fireEvent.keyPress(getByTestId('simulatorInput'), { key: 'Enter', code: 13, charCode: 13 });
274+
});
275+
276+
Object.defineProperty(window.navigator, 'onLine', {
277+
writable: true,
278+
value: false,
279+
});
280+
281+
// Trigger offline event to simulate real browser behavior
282+
window.dispatchEvent(new Event('offline'));
283+
284+
await waitFor(() => {
285+
expect(getByText('Simulator connection lost. Try to reload.')).toBeInTheDocument();
286+
});
287+
288+
// the banner should disappear when connection is restored
289+
Object.defineProperty(window.navigator, 'onLine', {
290+
writable: true,
291+
value: true,
292+
});
293+
294+
window.dispatchEvent(new Event('online'));
295+
296+
await waitFor(() => {
297+
expect(queryByText('Simulator connection lost. Try to reload.')).not.toBeInTheDocument();
298+
});
299+
});

src/components/simulator/Simulator.tsx

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { GUPSHUP_CALLBACK_URL } from 'config';
3131
import { ChatMessageType } from 'containers/Chat/ChatMessages/ChatMessage/ChatMessageType/ChatMessageType';
3232
import { TemplateButtons } from 'containers/Chat/ChatMessages/TemplateButtons/TemplateButtons';
3333
import { GET_SIMULATOR, RELEASE_SIMULATOR, SIMULATOR_SEARCH_QUERY } from 'graphql/queries/Simulator';
34-
// import { SIMULATOR_RELEASE_SUBSCRIPTION } from 'graphql/subscriptions/PeriodicInfo';
3534
import { getUserSession } from 'services/AuthService';
3635
import { setNotification } from 'common/notification';
3736
import setLogs from 'config/logs';
@@ -132,6 +131,7 @@ const Simulator = ({
132131
const [simulatedMessages, setSimulatedMessage] = useState<any>();
133132
const [isOpen, setIsOpen] = useState(false);
134133
const nodeRef = useRef<HTMLDivElement>(null!);
134+
const [isDisconnected, setIsDisconnected] = useState(false);
135135

136136
const client = useApolloClient();
137137
// Template listing
@@ -150,6 +150,33 @@ const Simulator = ({
150150
};
151151
// chat messages will be shown on simulator
152152
const isSimulatedMessage = true;
153+
154+
useEffect(() => {
155+
if (isPreviewMessage) return;
156+
157+
const handleOnline = () => {
158+
setIsDisconnected(false);
159+
};
160+
161+
const handleOffline = () => {
162+
setIsDisconnected(true);
163+
};
164+
165+
if (!navigator.onLine) {
166+
setIsDisconnected(true);
167+
}
168+
169+
window.addEventListener('online', handleOnline);
170+
window.addEventListener('offline', handleOffline);
171+
172+
return () => {
173+
if (!isPreviewMessage) {
174+
window.removeEventListener('online', handleOnline);
175+
window.removeEventListener('offline', handleOffline);
176+
}
177+
};
178+
}, [isPreviewMessage]);
179+
153180
const sendMessage = (senderDetails: Sender, interactivePayload?: any, templateValue?: any, messageUuid?: any) => {
154181
const sendMessageText = inputMessage === '' && message ? message : inputMessage;
155182

@@ -189,6 +216,7 @@ const Simulator = ({
189216
// add log's
190217
setLogs(`sendMessageText:${sendMessageText} GUPSHUP_CALLBACK_URL:${GUPSHUP_CALLBACK_URL}`, 'info');
191218
setLogs(error, 'error', true);
219+
setIsDisconnected(true);
192220
});
193221
setInputMessage('');
194222
};
@@ -210,13 +238,26 @@ const Simulator = ({
210238
}
211239
}
212240
},
241+
onError: (error) => {
242+
setLogs('SIMULATOR_RELEASE_SUBSCRIPTION error', 'error', true);
243+
setLogs(error, 'error', true);
244+
setIsDisconnected(true);
245+
},
213246
});
214247

215248
useSubscription(SIMULATOR_MESSAGE_SENT_SUBSCRIPTION, {
216249
variables,
217250
skip: isPreviewMessage,
218251
onData: ({ data: sentData }) => {
219252
setAllConversations(updateSimulatorConversations(allConversations, sentData, 'SENT'));
253+
if (isDisconnected) {
254+
setIsDisconnected(false);
255+
}
256+
},
257+
onError: (error) => {
258+
setLogs('SIMULATOR_MESSAGE_SENT_SUBSCRIPTION error', 'error', true);
259+
setLogs(error, 'error', true);
260+
setIsDisconnected(true);
220261
},
221262
});
222263

@@ -225,6 +266,15 @@ const Simulator = ({
225266
skip: isPreviewMessage,
226267
onData: ({ data: receivedData }) => {
227268
setAllConversations(updateSimulatorConversations(allConversations, receivedData, 'RECEIVED'));
269+
// Reset disconnected state on successful data
270+
if (isDisconnected) {
271+
setIsDisconnected(false);
272+
}
273+
},
274+
onError: (error) => {
275+
setLogs('SIMULATOR_MESSAGE_RECEIVED_SUBSCRIPTION error', 'error', true);
276+
setLogs(error, 'error', true);
277+
setIsDisconnected(true);
228278
},
229279
});
230280

@@ -271,6 +321,7 @@ const Simulator = ({
271321
// add log's
272322
setLogs(`sendMediaMessage:${type} GUPSHUP_CALLBACK_URL:${GUPSHUP_CALLBACK_URL}`, 'info');
273323
setLogs(error, 'error', true);
324+
setIsDisconnected(true);
274325
});
275326
};
276327

@@ -494,6 +545,10 @@ const Simulator = ({
494545
</ClickAwayListener>
495546
);
496547

548+
const disconnectionBanner = isDisconnected && !isPreviewMessage && (
549+
<div className={styles.DisconnectedBanner}>Simulator connection lost. Try to reload.</div>
550+
);
551+
497552
const simulator = (
498553
<Draggable nodeRef={nodeRef}>
499554
<div ref={nodeRef} data-testid="simulator-container" className={styles.SimContainer}>
@@ -532,6 +587,8 @@ const Simulator = ({
532587
<MoreVertIcon />
533588
</div>
534589
</div>
590+
{disconnectionBanner}
591+
535592
<div className={styles.Messages} ref={messageRef} data-testid="simulatedMessages">
536593
{simulatedMessages}
537594
</div>
@@ -549,7 +606,7 @@ const Simulator = ({
549606
}}
550607
value={inputMessage}
551608
placeholder="Type a message"
552-
disabled={isPreviewMessage}
609+
disabled={isPreviewMessage || isDisconnected}
553610
onChange={(event) => setInputMessage(event.target.value)}
554611
/>
555612
<AttachFileIcon
@@ -564,7 +621,7 @@ const Simulator = ({
564621
<Button
565622
variant="contained"
566623
className={styles.SendButton}
567-
disabled={isPreviewMessage}
624+
disabled={isPreviewMessage || isDisconnected}
568625
onClick={() => sendMessage(sender)}
569626
>
570627
<MicIcon />
@@ -607,6 +664,11 @@ const Simulator = ({
607664
id: searchData.search[0].contact.id,
608665
});
609666
}
667+
})
668+
.catch((error) => {
669+
setLogs('SIMULATOR_SEARCH_QUERY error', 'error', true);
670+
setLogs(error, 'error', true);
671+
setIsDisconnected(true);
610672
});
611673
} else {
612674
setNotification(
@@ -615,10 +677,14 @@ const Simulator = ({
615677
);
616678
}
617679
})
618-
.catch(() => {
680+
.catch((error) => {
619681
setNotification('Sorry! Failed to get simulator', 'warning');
682+
setLogs('GET_SIMULATOR error', 'error', true);
683+
setLogs(error, 'error', true);
684+
setIsDisconnected(true);
620685
});
621686
};
687+
622688
return isPreviewMessage ? (
623689
simulator
624690
) : simulatorId ? (

0 commit comments

Comments
 (0)