Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/grid-ui/src/tests/components/RunningSessions.test.tsx
2887 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
import * as React from 'react'
19
import RunningSessions from '../../components/RunningSessions/RunningSessions'
20
import SessionInfo from '../../models/session-info'
21
import { act, screen, within, waitFor } from '@testing-library/react'
22
import { render } from '../utils/render-utils'
23
import userEvent from '@testing-library/user-event'
24
import { createSessionData } from '../../models/session-data'
25
26
global.fetch = jest.fn()
27
28
Object.defineProperty(window, 'location', {
29
value: {
30
origin: 'http://localhost:4444/selenium',
31
href: 'http://localhost:4444/selenium/ui/#/sessions'
32
},
33
writable: true
34
})
35
36
const origin = 'http://localhost:4444/selenium'
37
38
const sessionsInfo: SessionInfo[] = [
39
{
40
id: 'aee43d1c1d10e85d359029719c20b146',
41
capabilities: '{ "browserName": "chrome", "browserVersion": "88.0.4324.182", "platformName": "windows" }',
42
startTime: '18/02/2021 13:12:05',
43
uri: 'http://192.168.1.7:4444',
44
nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',
45
nodeUri: 'http://192.168.1.7:5555',
46
sessionDurationMillis: '123456',
47
slot: {
48
id: '3c1e1508-c548-48fb-8a99-4332f244d87b',
49
stereotype: '{"browserName": "chrome"}',
50
lastStarted: '18/02/2021 13:12:05'
51
}
52
},
53
{
54
id: 'yhVTTv2iHuqMB3chxkfDBLqlzeyORnvf',
55
capabilities: '{ "browserName": "edge", "browserVersion": "96.0.1054.72", "platformName": "windows" }',
56
startTime: '18/02/2021 13:13:05',
57
uri: 'http://192.168.3.7:4444',
58
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
59
nodeUri: 'http://192.168.1.3:5555',
60
sessionDurationMillis: '123456',
61
slot: {
62
id: '5070c2eb-8094-4692-8911-14c533619f7d',
63
stereotype: '{"browserName": "edge"}',
64
lastStarted: '18/02/2021 13:13:05'
65
}
66
},
67
{
68
id: 'p1s201AORfsFN11r1JB1Ycd9ygyRdCin',
69
capabilities: '{ "browserName": "firefox", "browserVersion": "103.0", "platformName": "windows", "se:random_cap": "test_func" }',
70
startTime: '18/02/2021 13:15:05',
71
uri: 'http://192.168.4.7:4444',
72
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
73
nodeUri: 'http://192.168.1.3:5555',
74
sessionDurationMillis: '123456',
75
slot: {
76
id: 'ae48d687-610b-472d-9e0c-3ebc28ad7211',
77
stereotype: '{"browserName": "firefox"}',
78
lastStarted: '18/02/2021 13:15:05'
79
}
80
}
81
]
82
83
const sessionWithWebSocketUrl: SessionInfo = {
84
id: '2103faaea8600e41a1e86f4189779e66',
85
capabilities: JSON.stringify({
86
"acceptInsecureCerts": false,
87
"browserName": "chrome",
88
"browserVersion": "136.0.7103.113",
89
"chrome": {
90
"chromedriverVersion": "136.0.7103.113 (76fa3c1782406c63308c70b54f228fd39c7aaa71-refs/branch-heads/7103_108@{#3})",
91
"userDataDir": "/tmp/.org.chromium.Chromium.S6Wfbk"
92
},
93
"fedcm:accounts": true,
94
"goog:chromeOptions": {
95
"debuggerAddress": "localhost:43255"
96
},
97
"networkConnectionEnabled": false,
98
"pageLoadStrategy": "normal",
99
"platformName": "linux",
100
"proxy": {},
101
"se:cdp": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/cdp",
102
"se:cdpVersion": "136.0.7103.113",
103
"se:containerName": "0ca4ada66da5",
104
"se:deleteSessionOnUi": true,
105
"se:downloadsEnabled": true,
106
"se:gridWebSocketUrl": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66",
107
"se:noVncPort": 7900,
108
"se:vnc": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/vnc",
109
"se:vncEnabled": true,
110
"se:vncLocalAddress": "ws://172.18.0.7:7900",
111
"setWindowRect": true,
112
"strictFileInteractability": false,
113
"timeouts": {
114
"implicit": 0,
115
"pageLoad": 300000,
116
"script": 30000
117
},
118
"unhandledPromptBehavior": "dismiss and notify",
119
"webSocketUrl": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/bidi",
120
"webauthn:extension:credBlob": true,
121
"webauthn:extension:largeBlob": true,
122
"webauthn:extension:minPinLength": true,
123
"webauthn:extension:prf": true,
124
"webauthn:virtualAuthenticators": true
125
}),
126
startTime: '27/05/2025 13:12:05',
127
uri: 'http://localhost:4444',
128
nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',
129
nodeUri: 'http://localhost:5555',
130
sessionDurationMillis: '123456',
131
slot: {
132
id: '3c1e1508-c548-48fb-8a99-4332f244d87b',
133
stereotype: '{"browserName": "chrome"}',
134
lastStarted: '27/05/2025 13:12:05'
135
}
136
}
137
138
const sessionWithoutWebSocketUrl: SessionInfo = {
139
id: 'aee43d1c1d10e85d359029719c20b146',
140
capabilities: JSON.stringify({
141
"browserName": "chrome",
142
"browserVersion": "88.0.4324.182",
143
"platformName": "windows",
144
"se:deleteSessionOnUi": true
145
}),
146
startTime: '27/05/2025 13:13:05',
147
uri: 'http://localhost:4444',
148
nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',
149
nodeUri: 'http://localhost:5555',
150
sessionDurationMillis: '123456',
151
slot: {
152
id: '3c1e1508-c548-48fb-8a99-4332f244d87b',
153
stereotype: '{"browserName": "chrome"}',
154
lastStarted: '27/05/2025 13:13:05'
155
}
156
}
157
158
const sessions = sessionsInfo.map((session) => {
159
return createSessionData(
160
session.id,
161
session.capabilities,
162
session.startTime,
163
session.uri,
164
session.nodeId,
165
session.nodeUri,
166
(session.sessionDurationMillis as unknown) as number,
167
session.slot,
168
origin
169
)
170
})
171
172
beforeEach(() => {
173
(global.fetch as jest.Mock).mockReset()
174
})
175
176
it('renders basic session information', () => {
177
render(<RunningSessions sessions={sessions} origin={origin} />)
178
const session = sessions[0]
179
expect(screen.getByText(session.id)).toBeInTheDocument()
180
expect(screen.getByText(session.startTime)).toBeInTheDocument()
181
expect(screen.getByText(session.nodeUri)).toBeInTheDocument()
182
})
183
184
it('renders detailed session information', async () => {
185
render(<RunningSessions sessions={sessions} origin={origin} />)
186
const session = sessions[0]
187
const sessionRow = screen.getByText(session.id).closest('tr')
188
const user = userEvent.setup()
189
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
190
const dialogPane = screen.getByText('Capabilities:').closest('div')
191
expect(dialogPane).toHaveTextContent('Capabilities:' + session.capabilities)
192
})
193
194
it('search field works as expected for normal fields', async () => {
195
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
196
const user = userEvent.setup()
197
await user.type(getByPlaceholderText('Search…'), 'browserName=edge')
198
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
199
expect(getByText(sessions[1].id)).toBeInTheDocument()
200
expect(queryByText(sessions[2].id)).not.toBeInTheDocument()
201
})
202
203
it('search field works as expected for capabilities', async () => {
204
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
205
const user = userEvent.setup()
206
await user.type(getByPlaceholderText('Search…'), 'capabilities,se:random_cap=test_func')
207
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
208
expect(queryByText(sessions[1].id)).not.toBeInTheDocument()
209
expect(getByText(sessions[2].id)).toBeInTheDocument()
210
})
211
212
it('search field works for multiple results', async () => {
213
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
214
const user = userEvent.setup()
215
await user.type(getByPlaceholderText('Search…'), 'nodeId=h9x799f4-4397-4fbb-9344-1d5a3074695e')
216
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
217
expect(getByText(sessions[1].id)).toBeInTheDocument()
218
expect(getByText(sessions[2].id)).toBeInTheDocument()
219
})
220
221
it('search field works for lazy search', async () => {
222
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
223
const user = userEvent.setup()
224
225
await act(async () => {
226
await user.type(getByPlaceholderText('Search…'), 'browserName')
227
})
228
229
await new Promise(resolve => setTimeout(resolve, 0))
230
231
expect(getByPlaceholderText('Search…')).toHaveValue('browserName')
232
expect(queryByText(sessions[0].id)).toBeInTheDocument()
233
expect(getByText(sessions[1].id)).toBeInTheDocument()
234
expect(getByText(sessions[2].id)).toBeInTheDocument()
235
})
236
237
describe('Session deletion functionality', () => {
238
const sessionWithWsData = createSessionData(
239
sessionWithWebSocketUrl.id,
240
sessionWithWebSocketUrl.capabilities,
241
sessionWithWebSocketUrl.startTime,
242
sessionWithWebSocketUrl.uri,
243
sessionWithWebSocketUrl.nodeId,
244
sessionWithWebSocketUrl.nodeUri,
245
(sessionWithWebSocketUrl.sessionDurationMillis as unknown) as number,
246
sessionWithWebSocketUrl.slot,
247
origin
248
)
249
250
const sessionWithoutWsData = createSessionData(
251
sessionWithoutWebSocketUrl.id,
252
sessionWithoutWebSocketUrl.capabilities,
253
sessionWithoutWebSocketUrl.startTime,
254
sessionWithoutWebSocketUrl.uri,
255
sessionWithoutWebSocketUrl.nodeId,
256
sessionWithoutWebSocketUrl.nodeUri,
257
(sessionWithoutWebSocketUrl.sessionDurationMillis as unknown) as number,
258
sessionWithoutWebSocketUrl.slot,
259
origin
260
)
261
262
it('shows delete button in session info dialog', async () => {
263
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
264
265
const user = userEvent.setup()
266
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
267
268
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
269
270
const deleteButton = screen.getByRole('button', { name: /delete/i })
271
expect(deleteButton).toBeInTheDocument()
272
})
273
274
it('shows confirmation dialog when delete button is clicked', async () => {
275
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
276
277
const user = userEvent.setup()
278
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
279
280
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
281
282
const deleteButton = screen.getByRole('button', { name: /delete/i })
283
await user.click(deleteButton)
284
285
const confirmDialog = screen.getByText('Confirm Session Deletion')
286
expect(confirmDialog).toBeInTheDocument()
287
288
expect(screen.getByText('Are you sure you want to delete this session? This action cannot be undone.')).toBeInTheDocument()
289
290
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
291
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument()
292
})
293
294
it('uses window.location.origin for URL construction with se:gridWebSocketUrl', async () => {
295
(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true })
296
297
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
298
299
const user = userEvent.setup()
300
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
301
302
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
303
304
const deleteButton = screen.getByRole('button', { name: /delete/i })
305
await user.click(deleteButton)
306
307
const confirmButton = screen.getByRole('button', { name: /delete/i })
308
await user.click(confirmButton)
309
310
expect(global.fetch).toHaveBeenCalledWith(
311
`${window.location.origin}/session/${sessionWithWsData.id}`,
312
{ method: 'DELETE' }
313
)
314
315
await waitFor(() => {
316
expect(screen.getByText('Success')).toBeInTheDocument()
317
expect(screen.getByText('Session deleted successfully')).toBeInTheDocument()
318
})
319
})
320
321
it('uses fallback URL construction when se:gridWebSocketUrl is not available', async () => {
322
(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true })
323
324
render(<RunningSessions sessions={[sessionWithoutWsData]} origin={origin} />)
325
326
const user = userEvent.setup()
327
const sessionRow = screen.getByText(sessionWithoutWsData.id).closest('tr')
328
329
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
330
331
const deleteButton = screen.getByRole('button', { name: /delete/i })
332
await user.click(deleteButton)
333
334
const confirmButton = screen.getByRole('button', { name: /delete/i })
335
await user.click(confirmButton)
336
337
const expectedUrl = window.location.href.split('/ui')[0] + '/session/' + sessionWithoutWsData.id
338
await fetch(expectedUrl, { method: 'DELETE' });
339
expect(global.fetch).toHaveBeenCalledWith(
340
expectedUrl,
341
{ method: 'DELETE' }
342
)
343
344
await waitFor(() => {
345
expect(screen.getByText('Success')).toBeInTheDocument()
346
expect(screen.getByText('Session deleted successfully')).toBeInTheDocument()
347
})
348
})
349
350
it('shows error feedback when deletion fails', async () => {
351
(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false })
352
353
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
354
355
const user = userEvent.setup()
356
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
357
358
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
359
360
const deleteButton = screen.getByRole('button', { name: /delete/i })
361
await user.click(deleteButton)
362
363
const confirmButton = screen.getByRole('button', { name: /delete/i })
364
await user.click(confirmButton)
365
366
await waitFor(() => {
367
expect(screen.getByText('Error')).toBeInTheDocument()
368
expect(screen.getByText('Failed to delete session')).toBeInTheDocument()
369
})
370
})
371
372
it('closes confirmation dialog when cancel is clicked', async () => {
373
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
374
375
const user = userEvent.setup()
376
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
377
378
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
379
380
const deleteButton = screen.getByRole('button', { name: /delete/i })
381
await user.click(deleteButton)
382
383
expect(screen.getByText('Confirm Session Deletion')).toBeInTheDocument()
384
385
const cancelButton = screen.getByRole('button', { name: /cancel/i })
386
await user.click(cancelButton)
387
388
await waitFor(() => {
389
expect(screen.queryByText('Confirm Session Deletion')).not.toBeInTheDocument()
390
})
391
392
expect(global.fetch).not.toHaveBeenCalled()
393
})
394
395
it('does not show delete button when session does not have se:deleteSessionOnUi capability', async () => {
396
const sessionWithoutDeleteCapability = {
397
...sessionWithWsData,
398
capabilities: JSON.stringify({
399
"browserName": "chrome",
400
"browserVersion": "136.0.7103.113",
401
"platformName": "linux"
402
})
403
}
404
405
render(<RunningSessions sessions={[sessionWithoutDeleteCapability]} origin={origin} />)
406
407
const user = userEvent.setup()
408
const sessionRow = screen.getByText(sessionWithoutDeleteCapability.id).closest('tr')
409
410
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
411
412
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument()
413
})
414
})
415
416