Skip to content

Commit a166716

Browse files
committed
⚡️(frontend) close websocket connection when user change tab
When a user change to another tab, after a delay of "inactivity" we disconnect the user from the collaboration server. When the user come back we reconnect to the server again. It will reduce the connection to the collaboration server and reduce outburst during reconnection during a ingress ngnix restart.
1 parent 6fe0221 commit a166716

10 files changed

Lines changed: 429 additions & 277 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
### Added
1010

1111
- ⚡️(frontend) add skeleton on content loading #2254
12+
- ⚡️(frontend) close websocket connection when user change tab #2264
1213

1314
### Fixed
1415

env.d/development/common

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ COLLABORATION_SERVER_ORIGIN=http://localhost:3000
7878
COLLABORATION_SERVER_SECRET=my-secret
7979
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=true
8080
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
81+
COLLABORATION_WS_INACTIVITY_TIMEOUT=15 # Seconds
8182

8283
DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
8384
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/

src/frontend/apps/e2e/.env

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ PORT=3000
22
BASE_URL=http://localhost:3000
33
BASE_API_URL=http://localhost:8071/api/v1.0
44
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
5-
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=true
65
MEDIA_BASE_URL=http://localhost:8083
76
CUSTOM_SIGN_IN=false
87
IS_INSTANCE=false

src/frontend/apps/e2e/.env.example

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ PORT=3000
22
BASE_URL=http://localhost:3000
33
BASE_API_URL=http://localhost:8071/api/v1.0
44
COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
5-
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=true
65
MEDIA_BASE_URL=http://localhost:8083
76
IS_INSTANCE=false
87
CUSTOM_SIGN_IN=false
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import path from 'path';
2+
3+
import { expect, test } from '@playwright/test';
4+
5+
import { createDoc, overrideConfig, verifyDocName } from './utils-common';
6+
import { writeInEditor } from './utils-editor';
7+
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
8+
import { createRootSubPage } from './utils-sub-pages';
9+
10+
test.beforeEach(async ({ page }) => {
11+
await page.goto('/');
12+
});
13+
14+
test.describe('Doc Collaboration', () => {
15+
/**
16+
* We check:
17+
* - connection to the collaborative server
18+
* - signal of the backend to the collaborative server (connection should close)
19+
* - reconnection to the collaborative server
20+
*/
21+
test('checks the connection with collaborative server', async ({ page }) => {
22+
let webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
23+
return webSocket
24+
.url()
25+
.includes(`${process.env.COLLABORATION_WS_URL}?room=`);
26+
});
27+
28+
await page
29+
.getByRole('button', {
30+
name: 'New doc',
31+
})
32+
.click();
33+
34+
let webSocket = await webSocketPromise;
35+
expect(webSocket.url()).toContain(
36+
`${process.env.COLLABORATION_WS_URL}?room=`,
37+
);
38+
39+
// Is connected
40+
let framesentPromise = webSocket.waitForEvent('framesent');
41+
42+
await writeInEditor({ page, text: 'Hello World' });
43+
44+
let framesent = await framesentPromise;
45+
expect(framesent.payload).not.toBeNull();
46+
47+
await page.getByRole('button', { name: 'Share' }).click();
48+
49+
const selectVisibility = page.getByTestId('doc-visibility');
50+
51+
// When the visibility is changed, the ws should close the connection (backend signal)
52+
const wsClosePromise = webSocket.waitForEvent('close');
53+
54+
await selectVisibility.click();
55+
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
56+
57+
// Assert that the doc reconnects to the ws
58+
const wsClose = await wsClosePromise;
59+
expect(wsClose.isClosed()).toBeTruthy();
60+
61+
// Check the ws is connected again
62+
webSocket = await page.waitForEvent('websocket', (webSocket) => {
63+
return webSocket
64+
.url()
65+
.includes(`${process.env.COLLABORATION_WS_URL}?room=`);
66+
});
67+
framesentPromise = webSocket.waitForEvent('framesent');
68+
framesent = await framesentPromise;
69+
expect(framesent.payload).not.toBeNull();
70+
});
71+
72+
test('it cannot edit if viewer but see and can get resources', async ({
73+
page,
74+
browserName,
75+
}) => {
76+
const [docTitle] = await createDoc(page, 'doc-viewer', browserName, 1);
77+
await verifyDocName(page, docTitle);
78+
79+
await writeInEditor({ page, text: 'Hello World' });
80+
81+
await page.getByRole('button', { name: 'Share' }).click();
82+
await updateShareLink(page, 'Public', 'Reading');
83+
84+
// Close the modal
85+
await page.getByRole('button', { name: 'close' }).first().click();
86+
87+
const { otherPage, cleanup } = await connectOtherUserToDoc({
88+
browserName,
89+
docUrl: page.url(),
90+
withoutSignIn: true,
91+
docTitle,
92+
});
93+
94+
await expect(
95+
otherPage.getByLabel('It is the card information').getByText('Reader'),
96+
).toBeVisible();
97+
98+
// Cannot edit
99+
const editor = otherPage.locator('.ProseMirror');
100+
await expect(editor).toHaveAttribute('contenteditable', 'false');
101+
102+
// Owner add a image
103+
const fileChooserPromise = page.waitForEvent('filechooser');
104+
await page.locator('.bn-block-outer').last().fill('/');
105+
await page.getByText('Resizable image with caption').click();
106+
await page.getByText('Upload image').click();
107+
108+
const fileChooser = await fileChooserPromise;
109+
await fileChooser.setFiles(
110+
path.join(__dirname, 'assets/logo-suite-numerique.png'),
111+
);
112+
113+
// Owner see the image
114+
await expect(
115+
page.locator('.--docs--editor-container img.bn-visual-media').first(),
116+
).toBeVisible();
117+
118+
// Viewser see the image
119+
const viewerImg = otherPage
120+
.locator('.--docs--editor-container img.bn-visual-media')
121+
.first();
122+
await expect(viewerImg).toBeVisible({
123+
timeout: 10000,
124+
});
125+
126+
// Viewer can download the image
127+
await viewerImg.click();
128+
const downloadPromise = otherPage.waitForEvent('download');
129+
await otherPage.getByRole('button', { name: 'Download image' }).click();
130+
const download = await downloadPromise;
131+
expect(download.suggestedFilename()).toBe('logo-suite-numerique.png');
132+
133+
await cleanup();
134+
});
135+
136+
test('it checks block editing when not connected to collab server', async ({
137+
page,
138+
browserName,
139+
}) => {
140+
test.slow();
141+
142+
/**
143+
* The good port is 4444, but we want to simulate a not connected
144+
* collaborative server.
145+
* So we use a port that is not used by the collaborative server.
146+
* The server will not be able to connect to the collaborative server.
147+
*/
148+
await overrideConfig(page, {
149+
COLLABORATION_WS_URL: 'ws://localhost:5555/collaboration/ws/',
150+
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
151+
});
152+
153+
await page.goto('/');
154+
155+
const [parentTitle] = await createDoc(
156+
page,
157+
'editing-blocking',
158+
browserName,
159+
1,
160+
);
161+
162+
const card = page.getByLabel('It is the card information');
163+
await expect(
164+
card.getByText('Others are editing. Your network prevent changes.'),
165+
).toBeHidden();
166+
const editor = page.locator('.ProseMirror');
167+
168+
await expect(editor).toHaveAttribute('contenteditable', 'true');
169+
170+
let responseCanEditPromise = page.waitForResponse(
171+
(response) =>
172+
response.url().includes(`/can-edit/`) && response.status() === 200,
173+
);
174+
175+
await page.getByRole('button', { name: 'Share' }).click();
176+
177+
await updateShareLink(page, 'Public', 'Editing');
178+
179+
// Close the modal
180+
await page.getByRole('button', { name: 'close' }).first().click();
181+
182+
const urlParentDoc = page.url();
183+
184+
const { name: childTitle } = await createRootSubPage(
185+
page,
186+
browserName,
187+
'editing-blocking - child',
188+
);
189+
190+
let responseCanEdit = await responseCanEditPromise;
191+
expect(responseCanEdit.ok()).toBeTruthy();
192+
let jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
193+
expect(jsonCanEdit.can_edit).toBeTruthy();
194+
195+
const urlChildDoc = page.url();
196+
197+
/**
198+
* We open another browser that will connect to the collaborative server
199+
* and will block the current browser to edit the doc.
200+
*/
201+
const { otherPage, cleanup } = await connectOtherUserToDoc({
202+
browserName,
203+
docUrl: urlChildDoc,
204+
docTitle: childTitle,
205+
withoutSignIn: true,
206+
});
207+
208+
const webSocketPromise = otherPage.waitForEvent(
209+
'websocket',
210+
(webSocket) => {
211+
return webSocket
212+
.url()
213+
.includes(`${process.env.COLLABORATION_WS_URL}?room=`);
214+
},
215+
);
216+
217+
await otherPage.goto(urlChildDoc);
218+
219+
const webSocket = await webSocketPromise;
220+
expect(webSocket.url()).toContain(
221+
`${process.env.COLLABORATION_WS_URL}?room=`,
222+
);
223+
224+
await verifyDocName(otherPage, childTitle);
225+
226+
await page.reload();
227+
228+
responseCanEdit = await page.waitForResponse(
229+
(response) =>
230+
response.url().includes(`/can-edit/`) && response.status() === 200,
231+
);
232+
expect(responseCanEdit.ok()).toBeTruthy();
233+
234+
jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
235+
expect(jsonCanEdit.can_edit).toBeFalsy();
236+
237+
await expect(
238+
card.getByText('Others are editing. Your network prevent changes.'),
239+
).toBeVisible({
240+
timeout: 10000,
241+
});
242+
243+
await expect(editor).toHaveAttribute('contenteditable', 'false');
244+
245+
await expect(
246+
page.getByRole('textbox', { name: 'Document title' }),
247+
).toBeHidden();
248+
await expect(page.getByRole('heading', { name: childTitle })).toBeVisible();
249+
250+
await page.goto(urlParentDoc);
251+
252+
await verifyDocName(page, parentTitle);
253+
254+
await page.getByRole('button', { name: 'Share' }).click();
255+
256+
await page.getByTestId('doc-access-mode').click();
257+
await page.getByRole('menuitemradio', { name: 'Reading' }).click();
258+
259+
// Close the modal
260+
await page.getByRole('button', { name: 'close' }).first().click();
261+
262+
await page.goto(urlChildDoc);
263+
264+
await expect(editor).toHaveAttribute('contenteditable', 'true');
265+
266+
await expect(
267+
page.getByRole('textbox', { name: 'Document title' }),
268+
).toContainText(childTitle);
269+
await expect(page.getByRole('heading', { name: childTitle })).toBeHidden();
270+
271+
await expect(
272+
card.getByText('Others are editing. Your network prevent changes.'),
273+
).toBeHidden();
274+
275+
await cleanup();
276+
});
277+
278+
test('checks disconnection and reconnection when changing tab visibility', async ({
279+
page,
280+
}) => {
281+
await overrideConfig(page, {
282+
COLLABORATION_WS_INACTIVITY_TIMEOUT: 2, // 2 seconds for the test to be faster
283+
});
284+
285+
await page.goto('/');
286+
287+
let webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
288+
return webSocket
289+
.url()
290+
.includes(`${process.env.COLLABORATION_WS_URL}?room=`);
291+
});
292+
293+
await page
294+
.getByRole('button', {
295+
name: 'New doc',
296+
})
297+
.click();
298+
299+
let webSocket = await webSocketPromise;
300+
expect(webSocket.url()).toContain(
301+
`${process.env.COLLABORATION_WS_URL}?room=`,
302+
);
303+
304+
// Is connected
305+
let framesentPromise = webSocket.waitForEvent('framesent');
306+
307+
await writeInEditor({ page, text: 'Hello World' });
308+
309+
let framesent = await framesentPromise;
310+
expect(framesent.payload).not.toBeNull();
311+
312+
// When the visibility is changed, the ws should close the connection
313+
const wsClosePromise = webSocket.waitForEvent('close');
314+
315+
// Simulate the tab being hidden
316+
await page.evaluate(() => {
317+
Object.defineProperty(document, 'hidden', {
318+
value: true,
319+
writable: true,
320+
configurable: true,
321+
});
322+
document.dispatchEvent(new Event('visibilitychange'));
323+
});
324+
325+
// Assert the ws connection is closed after inactivity timeout
326+
const wsClose = await wsClosePromise;
327+
expect(wsClose.isClosed()).toBeTruthy();
328+
329+
// Check the ws is connected again
330+
webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
331+
return webSocket
332+
.url()
333+
.includes(`${process.env.COLLABORATION_WS_URL}?room=`);
334+
});
335+
336+
// Simulate the tab becoming visible again
337+
await page.evaluate(() => {
338+
Object.defineProperty(document, 'hidden', {
339+
value: false,
340+
writable: true,
341+
configurable: true,
342+
});
343+
document.dispatchEvent(new Event('visibilitychange'));
344+
});
345+
346+
webSocket = await webSocketPromise;
347+
framesentPromise = webSocket.waitForEvent('framesent');
348+
framesent = await framesentPromise;
349+
// Assert the ws connection is working again
350+
expect(framesent.payload).not.toBeNull();
351+
});
352+
});

0 commit comments

Comments
 (0)