blob: 11e278097335dc0888f28675abe80c2d46804afd [file] [log] [blame]
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import * as React from 'react'
import RunningSessions from '../../components/RunningSessions/RunningSessions'
import SessionInfo from '../../models/session-info'
import { act, screen, within, waitFor } from '@testing-library/react'
import { render } from '../utils/render-utils'
import userEvent from '@testing-library/user-event'
import { createSessionData } from '../../models/session-data'
global.fetch = jest.fn()
Object.defineProperty(window, 'location', {
value: {
origin: 'http://localhost:4444/selenium',
href: 'http://localhost:4444/selenium/ui/#/sessions'
},
writable: true
})
const origin = 'http://localhost:4444/selenium'
const sessionsInfo: SessionInfo[] = [
{
id: 'aee43d1c1d10e85d359029719c20b146',
capabilities: '{ "browserName": "chrome", "browserVersion": "88.0.4324.182", "platformName": "windows" }',
startTime: '18/02/2021 13:12:05',
uri: 'http://192.168.1.7:4444',
nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://192.168.1.7:5555',
sessionDurationMillis: '123456',
slot: {
id: '3c1e1508-c548-48fb-8a99-4332f244d87b',
stereotype: '{"browserName": "chrome"}',
lastStarted: '18/02/2021 13:12:05'
}
},
{
id: 'yhVTTv2iHuqMB3chxkfDBLqlzeyORnvf',
capabilities: '{ "browserName": "edge", "browserVersion": "96.0.1054.72", "platformName": "windows" }',
startTime: '18/02/2021 13:13:05',
uri: 'http://192.168.3.7:4444',
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://192.168.1.3:5555',
sessionDurationMillis: '123456',
slot: {
id: '5070c2eb-8094-4692-8911-14c533619f7d',
stereotype: '{"browserName": "edge"}',
lastStarted: '18/02/2021 13:13:05'
}
},
{
id: 'p1s201AORfsFN11r1JB1Ycd9ygyRdCin',
capabilities: '{ "browserName": "firefox", "browserVersion": "103.0", "platformName": "windows", "se:random_cap": "test_func" }',
startTime: '18/02/2021 13:15:05',
uri: 'http://192.168.4.7:4444',
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://192.168.1.3:5555',
sessionDurationMillis: '123456',
slot: {
id: 'ae48d687-610b-472d-9e0c-3ebc28ad7211',
stereotype: '{"browserName": "firefox"}',
lastStarted: '18/02/2021 13:15:05'
}
}
]
const sessionWithWebSocketUrl: SessionInfo = {
id: '2103faaea8600e41a1e86f4189779e66',
capabilities: JSON.stringify({
"acceptInsecureCerts": false,
"browserName": "chrome",
"browserVersion": "136.0.7103.113",
"chrome": {
"chromedriverVersion": "136.0.7103.113 (76fa3c1782406c63308c70b54f228fd39c7aaa71-refs/branch-heads/7103_108@{#3})",
"userDataDir": "/tmp/.org.chromium.Chromium.S6Wfbk"
},
"fedcm:accounts": true,
"goog:chromeOptions": {
"debuggerAddress": "localhost:43255"
},
"networkConnectionEnabled": false,
"pageLoadStrategy": "normal",
"platformName": "linux",
"proxy": {},
"se:cdp": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/cdp",
"se:cdpVersion": "136.0.7103.113",
"se:containerName": "0ca4ada66da5",
"se:deleteSessionOnUi": true,
"se:downloadsEnabled": true,
"se:gridWebSocketUrl": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66",
"se:noVncPort": 7900,
"se:vnc": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/vnc",
"se:vncEnabled": true,
"se:vncLocalAddress": "ws://172.18.0.7:7900",
"setWindowRect": true,
"strictFileInteractability": false,
"timeouts": {
"implicit": 0,
"pageLoad": 300000,
"script": 30000
},
"unhandledPromptBehavior": "dismiss and notify",
"webSocketUrl": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/bidi",
"webauthn:extension:credBlob": true,
"webauthn:extension:largeBlob": true,
"webauthn:extension:minPinLength": true,
"webauthn:extension:prf": true,
"webauthn:virtualAuthenticators": true
}),
startTime: '27/05/2025 13:12:05',
uri: 'http://localhost:4444',
nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://localhost:5555',
sessionDurationMillis: '123456',
slot: {
id: '3c1e1508-c548-48fb-8a99-4332f244d87b',
stereotype: '{"browserName": "chrome"}',
lastStarted: '27/05/2025 13:12:05'
}
}
const sessionWithoutWebSocketUrl: SessionInfo = {
id: 'aee43d1c1d10e85d359029719c20b146',
capabilities: JSON.stringify({
"browserName": "chrome",
"browserVersion": "88.0.4324.182",
"platformName": "windows",
"se:deleteSessionOnUi": true
}),
startTime: '27/05/2025 13:13:05',
uri: 'http://localhost:4444',
nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://localhost:5555',
sessionDurationMillis: '123456',
slot: {
id: '3c1e1508-c548-48fb-8a99-4332f244d87b',
stereotype: '{"browserName": "chrome"}',
lastStarted: '27/05/2025 13:13:05'
}
}
const sessions = sessionsInfo.map((session) => {
return createSessionData(
session.id,
session.capabilities,
session.startTime,
session.uri,
session.nodeId,
session.nodeUri,
(session.sessionDurationMillis as unknown) as number,
session.slot,
origin
)
})
beforeEach(() => {
(global.fetch as jest.Mock).mockReset()
})
it('renders basic session information', () => {
render(<RunningSessions sessions={sessions} origin={origin} />)
const session = sessions[0]
expect(screen.getByText(session.id)).toBeInTheDocument()
expect(screen.getByText(session.startTime)).toBeInTheDocument()
expect(screen.getByText(session.nodeUri)).toBeInTheDocument()
})
it('renders detailed session information', async () => {
render(<RunningSessions sessions={sessions} origin={origin} />)
const session = sessions[0]
const sessionRow = screen.getByText(session.id).closest('tr')
const user = userEvent.setup()
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const dialogPane = screen.getByText('Capabilities:').closest('div')
expect(dialogPane).toHaveTextContent('Capabilities:' + session.capabilities)
})
it('search field works as expected for normal fields', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('Search…'), 'browserName=edge')
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
expect(getByText(sessions[1].id)).toBeInTheDocument()
expect(queryByText(sessions[2].id)).not.toBeInTheDocument()
})
it('search field works as expected for capabilities', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('Search…'), 'capabilities,se:random_cap=test_func')
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
expect(queryByText(sessions[1].id)).not.toBeInTheDocument()
expect(getByText(sessions[2].id)).toBeInTheDocument()
})
it('search field works for multiple results', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('Search…'), 'nodeId=h9x799f4-4397-4fbb-9344-1d5a3074695e')
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
expect(getByText(sessions[1].id)).toBeInTheDocument()
expect(getByText(sessions[2].id)).toBeInTheDocument()
})
it('search field works for lazy search', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await act(async () => {
await user.type(getByPlaceholderText('Search…'), 'browserName')
})
await new Promise(resolve => setTimeout(resolve, 0))
expect(getByPlaceholderText('Search…')).toHaveValue('browserName')
expect(queryByText(sessions[0].id)).toBeInTheDocument()
expect(getByText(sessions[1].id)).toBeInTheDocument()
expect(getByText(sessions[2].id)).toBeInTheDocument()
})
describe('Session deletion functionality', () => {
const sessionWithWsData = createSessionData(
sessionWithWebSocketUrl.id,
sessionWithWebSocketUrl.capabilities,
sessionWithWebSocketUrl.startTime,
sessionWithWebSocketUrl.uri,
sessionWithWebSocketUrl.nodeId,
sessionWithWebSocketUrl.nodeUri,
(sessionWithWebSocketUrl.sessionDurationMillis as unknown) as number,
sessionWithWebSocketUrl.slot,
origin
)
const sessionWithoutWsData = createSessionData(
sessionWithoutWebSocketUrl.id,
sessionWithoutWebSocketUrl.capabilities,
sessionWithoutWebSocketUrl.startTime,
sessionWithoutWebSocketUrl.uri,
sessionWithoutWebSocketUrl.nodeId,
sessionWithoutWebSocketUrl.nodeUri,
(sessionWithoutWebSocketUrl.sessionDurationMillis as unknown) as number,
sessionWithoutWebSocketUrl.slot,
origin
)
it('shows delete button in session info dialog', async () => {
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const deleteButton = screen.getByRole('button', { name: /delete/i })
expect(deleteButton).toBeInTheDocument()
})
it('shows confirmation dialog when delete button is clicked', async () => {
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const deleteButton = screen.getByRole('button', { name: /delete/i })
await user.click(deleteButton)
const confirmDialog = screen.getByText('Confirm Session Deletion')
expect(confirmDialog).toBeInTheDocument()
expect(screen.getByText('Are you sure you want to delete this session? This action cannot be undone.')).toBeInTheDocument()
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument()
})
it('uses window.location.origin for URL construction with se:gridWebSocketUrl', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true })
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const deleteButton = screen.getByRole('button', { name: /delete/i })
await user.click(deleteButton)
const confirmButton = screen.getByRole('button', { name: /delete/i })
await user.click(confirmButton)
expect(global.fetch).toHaveBeenCalledWith(
`${window.location.origin}/session/${sessionWithWsData.id}`,
{ method: 'DELETE' }
)
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument()
expect(screen.getByText('Session deleted successfully')).toBeInTheDocument()
})
})
it('uses fallback URL construction when se:gridWebSocketUrl is not available', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true })
render(<RunningSessions sessions={[sessionWithoutWsData]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithoutWsData.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const deleteButton = screen.getByRole('button', { name: /delete/i })
await user.click(deleteButton)
const confirmButton = screen.getByRole('button', { name: /delete/i })
await user.click(confirmButton)
const expectedUrl = window.location.href.split('/ui')[0] + '/session/' + sessionWithoutWsData.id
await fetch(expectedUrl, { method: 'DELETE' });
expect(global.fetch).toHaveBeenCalledWith(
expectedUrl,
{ method: 'DELETE' }
)
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument()
expect(screen.getByText('Session deleted successfully')).toBeInTheDocument()
})
})
it('shows error feedback when deletion fails', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false })
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const deleteButton = screen.getByRole('button', { name: /delete/i })
await user.click(deleteButton)
const confirmButton = screen.getByRole('button', { name: /delete/i })
await user.click(confirmButton)
await waitFor(() => {
expect(screen.getByText('Error')).toBeInTheDocument()
expect(screen.getByText('Failed to delete session')).toBeInTheDocument()
})
})
it('closes confirmation dialog when cancel is clicked', async () => {
render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const deleteButton = screen.getByRole('button', { name: /delete/i })
await user.click(deleteButton)
expect(screen.getByText('Confirm Session Deletion')).toBeInTheDocument()
const cancelButton = screen.getByRole('button', { name: /cancel/i })
await user.click(cancelButton)
await waitFor(() => {
expect(screen.queryByText('Confirm Session Deletion')).not.toBeInTheDocument()
})
expect(global.fetch).not.toHaveBeenCalled()
})
it('does not show delete button when session does not have se:deleteSessionOnUi capability', async () => {
const sessionWithoutDeleteCapability = {
...sessionWithWsData,
capabilities: JSON.stringify({
"browserName": "chrome",
"browserVersion": "136.0.7103.113",
"platformName": "linux"
})
}
render(<RunningSessions sessions={[sessionWithoutDeleteCapability]} origin={origin} />)
const user = userEvent.setup()
const sessionRow = screen.getByText(sessionWithoutDeleteCapability.id).closest('tr')
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument()
})
})