Path: blob/trunk/javascript/grid-ui/src/tests/components/RunningSessions.test.tsx
2887 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617import * as React from 'react'18import RunningSessions from '../../components/RunningSessions/RunningSessions'19import SessionInfo from '../../models/session-info'20import { act, screen, within, waitFor } from '@testing-library/react'21import { render } from '../utils/render-utils'22import userEvent from '@testing-library/user-event'23import { createSessionData } from '../../models/session-data'2425global.fetch = jest.fn()2627Object.defineProperty(window, 'location', {28value: {29origin: 'http://localhost:4444/selenium',30href: 'http://localhost:4444/selenium/ui/#/sessions'31},32writable: true33})3435const origin = 'http://localhost:4444/selenium'3637const sessionsInfo: SessionInfo[] = [38{39id: 'aee43d1c1d10e85d359029719c20b146',40capabilities: '{ "browserName": "chrome", "browserVersion": "88.0.4324.182", "platformName": "windows" }',41startTime: '18/02/2021 13:12:05',42uri: 'http://192.168.1.7:4444',43nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',44nodeUri: 'http://192.168.1.7:5555',45sessionDurationMillis: '123456',46slot: {47id: '3c1e1508-c548-48fb-8a99-4332f244d87b',48stereotype: '{"browserName": "chrome"}',49lastStarted: '18/02/2021 13:12:05'50}51},52{53id: 'yhVTTv2iHuqMB3chxkfDBLqlzeyORnvf',54capabilities: '{ "browserName": "edge", "browserVersion": "96.0.1054.72", "platformName": "windows" }',55startTime: '18/02/2021 13:13:05',56uri: 'http://192.168.3.7:4444',57nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',58nodeUri: 'http://192.168.1.3:5555',59sessionDurationMillis: '123456',60slot: {61id: '5070c2eb-8094-4692-8911-14c533619f7d',62stereotype: '{"browserName": "edge"}',63lastStarted: '18/02/2021 13:13:05'64}65},66{67id: 'p1s201AORfsFN11r1JB1Ycd9ygyRdCin',68capabilities: '{ "browserName": "firefox", "browserVersion": "103.0", "platformName": "windows", "se:random_cap": "test_func" }',69startTime: '18/02/2021 13:15:05',70uri: 'http://192.168.4.7:4444',71nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',72nodeUri: 'http://192.168.1.3:5555',73sessionDurationMillis: '123456',74slot: {75id: 'ae48d687-610b-472d-9e0c-3ebc28ad7211',76stereotype: '{"browserName": "firefox"}',77lastStarted: '18/02/2021 13:15:05'78}79}80]8182const sessionWithWebSocketUrl: SessionInfo = {83id: '2103faaea8600e41a1e86f4189779e66',84capabilities: JSON.stringify({85"acceptInsecureCerts": false,86"browserName": "chrome",87"browserVersion": "136.0.7103.113",88"chrome": {89"chromedriverVersion": "136.0.7103.113 (76fa3c1782406c63308c70b54f228fd39c7aaa71-refs/branch-heads/7103_108@{#3})",90"userDataDir": "/tmp/.org.chromium.Chromium.S6Wfbk"91},92"fedcm:accounts": true,93"goog:chromeOptions": {94"debuggerAddress": "localhost:43255"95},96"networkConnectionEnabled": false,97"pageLoadStrategy": "normal",98"platformName": "linux",99"proxy": {},100"se:cdp": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/cdp",101"se:cdpVersion": "136.0.7103.113",102"se:containerName": "0ca4ada66da5",103"se:deleteSessionOnUi": true,104"se:downloadsEnabled": true,105"se:gridWebSocketUrl": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66",106"se:noVncPort": 7900,107"se:vnc": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/vnc",108"se:vncEnabled": true,109"se:vncLocalAddress": "ws://172.18.0.7:7900",110"setWindowRect": true,111"strictFileInteractability": false,112"timeouts": {113"implicit": 0,114"pageLoad": 300000,115"script": 30000116},117"unhandledPromptBehavior": "dismiss and notify",118"webSocketUrl": "ws://localhost:4444/selenium/session/2103faaea8600e41a1e86f4189779e66/se/bidi",119"webauthn:extension:credBlob": true,120"webauthn:extension:largeBlob": true,121"webauthn:extension:minPinLength": true,122"webauthn:extension:prf": true,123"webauthn:virtualAuthenticators": true124}),125startTime: '27/05/2025 13:12:05',126uri: 'http://localhost:4444',127nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',128nodeUri: 'http://localhost:5555',129sessionDurationMillis: '123456',130slot: {131id: '3c1e1508-c548-48fb-8a99-4332f244d87b',132stereotype: '{"browserName": "chrome"}',133lastStarted: '27/05/2025 13:12:05'134}135}136137const sessionWithoutWebSocketUrl: SessionInfo = {138id: 'aee43d1c1d10e85d359029719c20b146',139capabilities: JSON.stringify({140"browserName": "chrome",141"browserVersion": "88.0.4324.182",142"platformName": "windows",143"se:deleteSessionOnUi": true144}),145startTime: '27/05/2025 13:13:05',146uri: 'http://localhost:4444',147nodeId: '9fe799f4-4397-4fbb-9344-1d5a3074695e',148nodeUri: 'http://localhost:5555',149sessionDurationMillis: '123456',150slot: {151id: '3c1e1508-c548-48fb-8a99-4332f244d87b',152stereotype: '{"browserName": "chrome"}',153lastStarted: '27/05/2025 13:13:05'154}155}156157const sessions = sessionsInfo.map((session) => {158return createSessionData(159session.id,160session.capabilities,161session.startTime,162session.uri,163session.nodeId,164session.nodeUri,165(session.sessionDurationMillis as unknown) as number,166session.slot,167origin168)169})170171beforeEach(() => {172(global.fetch as jest.Mock).mockReset()173})174175it('renders basic session information', () => {176render(<RunningSessions sessions={sessions} origin={origin} />)177const session = sessions[0]178expect(screen.getByText(session.id)).toBeInTheDocument()179expect(screen.getByText(session.startTime)).toBeInTheDocument()180expect(screen.getByText(session.nodeUri)).toBeInTheDocument()181})182183it('renders detailed session information', async () => {184render(<RunningSessions sessions={sessions} origin={origin} />)185const session = sessions[0]186const sessionRow = screen.getByText(session.id).closest('tr')187const user = userEvent.setup()188await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))189const dialogPane = screen.getByText('Capabilities:').closest('div')190expect(dialogPane).toHaveTextContent('Capabilities:' + session.capabilities)191})192193it('search field works as expected for normal fields', async () => {194const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)195const user = userEvent.setup()196await user.type(getByPlaceholderText('Search…'), 'browserName=edge')197expect(queryByText(sessions[0].id)).not.toBeInTheDocument()198expect(getByText(sessions[1].id)).toBeInTheDocument()199expect(queryByText(sessions[2].id)).not.toBeInTheDocument()200})201202it('search field works as expected for capabilities', async () => {203const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)204const user = userEvent.setup()205await user.type(getByPlaceholderText('Search…'), 'capabilities,se:random_cap=test_func')206expect(queryByText(sessions[0].id)).not.toBeInTheDocument()207expect(queryByText(sessions[1].id)).not.toBeInTheDocument()208expect(getByText(sessions[2].id)).toBeInTheDocument()209})210211it('search field works for multiple results', async () => {212const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)213const user = userEvent.setup()214await user.type(getByPlaceholderText('Search…'), 'nodeId=h9x799f4-4397-4fbb-9344-1d5a3074695e')215expect(queryByText(sessions[0].id)).not.toBeInTheDocument()216expect(getByText(sessions[1].id)).toBeInTheDocument()217expect(getByText(sessions[2].id)).toBeInTheDocument()218})219220it('search field works for lazy search', async () => {221const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)222const user = userEvent.setup()223224await act(async () => {225await user.type(getByPlaceholderText('Search…'), 'browserName')226})227228await new Promise(resolve => setTimeout(resolve, 0))229230expect(getByPlaceholderText('Search…')).toHaveValue('browserName')231expect(queryByText(sessions[0].id)).toBeInTheDocument()232expect(getByText(sessions[1].id)).toBeInTheDocument()233expect(getByText(sessions[2].id)).toBeInTheDocument()234})235236describe('Session deletion functionality', () => {237const sessionWithWsData = createSessionData(238sessionWithWebSocketUrl.id,239sessionWithWebSocketUrl.capabilities,240sessionWithWebSocketUrl.startTime,241sessionWithWebSocketUrl.uri,242sessionWithWebSocketUrl.nodeId,243sessionWithWebSocketUrl.nodeUri,244(sessionWithWebSocketUrl.sessionDurationMillis as unknown) as number,245sessionWithWebSocketUrl.slot,246origin247)248249const sessionWithoutWsData = createSessionData(250sessionWithoutWebSocketUrl.id,251sessionWithoutWebSocketUrl.capabilities,252sessionWithoutWebSocketUrl.startTime,253sessionWithoutWebSocketUrl.uri,254sessionWithoutWebSocketUrl.nodeId,255sessionWithoutWebSocketUrl.nodeUri,256(sessionWithoutWebSocketUrl.sessionDurationMillis as unknown) as number,257sessionWithoutWebSocketUrl.slot,258origin259)260261it('shows delete button in session info dialog', async () => {262render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)263264const user = userEvent.setup()265const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')266267await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))268269const deleteButton = screen.getByRole('button', { name: /delete/i })270expect(deleteButton).toBeInTheDocument()271})272273it('shows confirmation dialog when delete button is clicked', async () => {274render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)275276const user = userEvent.setup()277const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')278279await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))280281const deleteButton = screen.getByRole('button', { name: /delete/i })282await user.click(deleteButton)283284const confirmDialog = screen.getByText('Confirm Session Deletion')285expect(confirmDialog).toBeInTheDocument()286287expect(screen.getByText('Are you sure you want to delete this session? This action cannot be undone.')).toBeInTheDocument()288289expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()290expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument()291})292293it('uses window.location.origin for URL construction with se:gridWebSocketUrl', async () => {294(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true })295296render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)297298const user = userEvent.setup()299const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')300301await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))302303const deleteButton = screen.getByRole('button', { name: /delete/i })304await user.click(deleteButton)305306const confirmButton = screen.getByRole('button', { name: /delete/i })307await user.click(confirmButton)308309expect(global.fetch).toHaveBeenCalledWith(310`${window.location.origin}/session/${sessionWithWsData.id}`,311{ method: 'DELETE' }312)313314await waitFor(() => {315expect(screen.getByText('Success')).toBeInTheDocument()316expect(screen.getByText('Session deleted successfully')).toBeInTheDocument()317})318})319320it('uses fallback URL construction when se:gridWebSocketUrl is not available', async () => {321(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true })322323render(<RunningSessions sessions={[sessionWithoutWsData]} origin={origin} />)324325const user = userEvent.setup()326const sessionRow = screen.getByText(sessionWithoutWsData.id).closest('tr')327328await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))329330const deleteButton = screen.getByRole('button', { name: /delete/i })331await user.click(deleteButton)332333const confirmButton = screen.getByRole('button', { name: /delete/i })334await user.click(confirmButton)335336const expectedUrl = window.location.href.split('/ui')[0] + '/session/' + sessionWithoutWsData.id337await fetch(expectedUrl, { method: 'DELETE' });338expect(global.fetch).toHaveBeenCalledWith(339expectedUrl,340{ method: 'DELETE' }341)342343await waitFor(() => {344expect(screen.getByText('Success')).toBeInTheDocument()345expect(screen.getByText('Session deleted successfully')).toBeInTheDocument()346})347})348349it('shows error feedback when deletion fails', async () => {350(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false })351352render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)353354const user = userEvent.setup()355const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')356357await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))358359const deleteButton = screen.getByRole('button', { name: /delete/i })360await user.click(deleteButton)361362const confirmButton = screen.getByRole('button', { name: /delete/i })363await user.click(confirmButton)364365await waitFor(() => {366expect(screen.getByText('Error')).toBeInTheDocument()367expect(screen.getByText('Failed to delete session')).toBeInTheDocument()368})369})370371it('closes confirmation dialog when cancel is clicked', async () => {372render(<RunningSessions sessions={[sessionWithWsData]} origin={origin} />)373374const user = userEvent.setup()375const sessionRow = screen.getByText(sessionWithWsData.id).closest('tr')376377await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))378379const deleteButton = screen.getByRole('button', { name: /delete/i })380await user.click(deleteButton)381382expect(screen.getByText('Confirm Session Deletion')).toBeInTheDocument()383384const cancelButton = screen.getByRole('button', { name: /cancel/i })385await user.click(cancelButton)386387await waitFor(() => {388expect(screen.queryByText('Confirm Session Deletion')).not.toBeInTheDocument()389})390391expect(global.fetch).not.toHaveBeenCalled()392})393394it('does not show delete button when session does not have se:deleteSessionOnUi capability', async () => {395const sessionWithoutDeleteCapability = {396...sessionWithWsData,397capabilities: JSON.stringify({398"browserName": "chrome",399"browserVersion": "136.0.7103.113",400"platformName": "linux"401})402}403404render(<RunningSessions sessions={[sessionWithoutDeleteCapability]} origin={origin} />)405406const user = userEvent.setup()407const sessionRow = screen.getByText(sessionWithoutDeleteCapability.id).closest('tr')408409await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))410411expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument()412})413})414415416