Path: blob/trunk/javascript/grid-ui/src/tests/components/Overview.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 { render, screen, within } from '@testing-library/react'19import userEvent from '@testing-library/user-event'20import '@testing-library/jest-dom'21import { MockedProvider } from '@apollo/client/testing'22import Overview from '../../screens/Overview/Overview'23import { NODES_QUERY } from '../../graphql/nodes'24import { GRID_SESSIONS_QUERY } from '../../graphql/sessions'2526jest.mock('../../components/LiveView/LiveView', () => {27return {28__esModule: true,29default: React.forwardRef((props: { url: string, scaleViewport?: boolean, onClose?: () => void }, ref) => {30React.useImperativeHandle(ref, () => ({31disconnect: jest.fn()32}))33return <div data-testid="mock-live-view" data-url={props.url}>LiveView Mock</div>34})35}36})3738const mockNodesData = {39nodesInfo: {40nodes: [41{42id: 'node1',43uri: 'http://192.168.1.10:4444',44status: 'UP',45maxSession: 5,46slotCount: 5,47version: '4.0.0',48osInfo: {49name: 'Linux',50version: '5.4.0',51arch: 'x86_64'52},53sessionCount: 1,54stereotypes: JSON.stringify([55{56stereotype: {57browserName: 'chrome',58browserVersion: '88.0',59platformName: 'linux'60},61slots: 562}63])64},65{66id: 'node2',67uri: 'http://192.168.1.11:4444',68status: 'UP',69maxSession: 5,70slotCount: 5,71version: '4.0.0',72osInfo: {73name: 'Windows',74version: '10',75arch: 'x86_64'76},77sessionCount: 2,78stereotypes: JSON.stringify([79{80stereotype: {81browserName: 'firefox',82browserVersion: '78.0',83platformName: 'windows'84},85slots: 586}87])88}89]90}91}9293const mockSessionsData = {94sessionsInfo: {95sessions: [96{97id: 'session1',98nodeId: 'node1',99capabilities: JSON.stringify({100browserName: 'chrome',101browserVersion: '88.0',102platformName: 'linux',103'se:vnc': 'ws://192.168.1.10:5900/websockify'104}),105startTime: '2023-01-01T00:00:00Z',106uri: 'http://192.168.1.10:4444/session/session1',107nodeUri: 'http://192.168.1.10:4444',108sessionDurationMillis: 60000,109slot: {110id: 'slot1',111stereotype: '{"browserName":"chrome"}',112lastStarted: '2023-01-01T00:00:00Z'113}114},115{116id: 'session2',117nodeId: 'node2',118capabilities: JSON.stringify({119browserName: 'firefox',120browserVersion: '78.0',121platformName: 'windows'122}),123startTime: '2023-01-01T00:00:00Z',124uri: 'http://192.168.1.11:4444/session/session2',125nodeUri: 'http://192.168.1.11:4444',126sessionDurationMillis: 60000,127slot: {128id: 'slot2',129stereotype: '{"browserName":"firefox"}',130lastStarted: '2023-01-01T00:00:00Z'131}132}133],134sessionQueueRequests: []135}136}137138const mocks = [139{140request: {141query: NODES_QUERY142},143result: {144data: mockNodesData145}146},147{148request: {149query: GRID_SESSIONS_QUERY150},151result: {152data: mockSessionsData153}154},155{156request: {157query: GRID_SESSIONS_QUERY158},159result: {160data: mockSessionsData161}162},163{164request: {165query: GRID_SESSIONS_QUERY166},167result: {168data: mockSessionsData169}170}171]172173describe('Overview component', () => {174beforeEach(() => {175Object.defineProperty(window, 'location', {176value: {177origin: 'http://localhost:4444'178},179writable: true180})181})182183it('renders loading state initially', () => {184render(185<MockedProvider mocks={mocks} addTypename={false}>186<Overview />187</MockedProvider>188)189190expect(screen.getByRole('progressbar')).toBeInTheDocument()191})192193it('renders nodes when data is loaded', async () => {194render(195<MockedProvider mocks={mocks} addTypename={false}>196<Overview />197</MockedProvider>198)199200await screen.findByText('http://192.168.1.10:4444')201202expect(screen.getByText('http://192.168.1.10:4444')).toBeInTheDocument()203expect(screen.getByText('http://192.168.1.11:4444')).toBeInTheDocument()204})205206it('renders sort controls', async () => {207render(208<MockedProvider mocks={mocks} addTypename={false}>209<Overview />210</MockedProvider>211)212213await screen.findByText('http://192.168.1.10:4444')214215expect(screen.getAllByText('Sort By').length).toBeGreaterThan(0)216expect(screen.getByText('Descending')).toBeInTheDocument()217})218219it('changes sort option when selected', async () => {220render(221<MockedProvider mocks={mocks} addTypename={false}>222<Overview />223</MockedProvider>224)225226await screen.findByText('http://192.168.1.10:4444')227228const user = userEvent.setup()229const selectElement = screen.getByRole('combobox')230await user.click(selectElement)231232await user.click(screen.getByText('Browser Name'))233234expect(selectElement).toHaveTextContent('Browser Name')235})236237it('toggles sort order when descending checkbox is clicked', async () => {238render(239<MockedProvider mocks={mocks} addTypename={false}>240<Overview />241</MockedProvider>242)243244await screen.findByText('http://192.168.1.10:4444')245246const user = userEvent.setup()247const descendingLabel = screen.getByText('Descending')248const checkbox = descendingLabel.closest('label')?.querySelector('input[type="checkbox"]')249250expect(checkbox).not.toBeNull()251if (checkbox) {252await user.click(checkbox)253expect(checkbox).toBeChecked()254}255})256257it('sorts nodes by URI when selected', async () => {258render(259<MockedProvider mocks={mocks} addTypename={false}>260<Overview />261</MockedProvider>262)263264await screen.findByText('http://192.168.1.10:4444')265266const user = userEvent.setup()267const selectElement = screen.getByRole('combobox')268await user.click(selectElement)269await user.click(screen.getByText('URI'))270271// After sorting by URI, node1 should appear before node2272const nodeUris = screen.getAllByText(/http:\/\/192\.168\.1\.\d+:4444/)273expect(nodeUris[0]).toHaveTextContent('http://192.168.1.10:4444')274expect(nodeUris[1]).toHaveTextContent('http://192.168.1.11:4444')275})276277it('sorts nodes by URI in descending order when selected', async () => {278render(279<MockedProvider mocks={mocks} addTypename={false}>280<Overview />281</MockedProvider>282)283284await screen.findByText('http://192.168.1.10:4444')285286const user = userEvent.setup()287const selectElement = screen.getByRole('combobox')288await user.click(selectElement)289await user.click(screen.getByText('URI'))290291const descendingLabel = screen.getByText('Descending')292const checkbox = descendingLabel.closest('label')?.querySelector('input[type="checkbox"]')293expect(checkbox).not.toBeNull()294if (checkbox) {295await user.click(checkbox)296expect(checkbox).toBeChecked()297}298299// After sorting by URI descending, node2 should appear before node1300const nodeUris = screen.getAllByText(/http:\/\/192\.168\.1\.\d+:4444/)301expect(nodeUris[0]).toHaveTextContent('http://192.168.1.11:4444')302expect(nodeUris[1]).toHaveTextContent('http://192.168.1.10:4444')303})304305it('renders live view icon for node with VNC session', async () => {306render(307<MockedProvider mocks={mocks} addTypename={false}>308<Overview />309</MockedProvider>310)311312await screen.findByText('http://192.168.1.10:4444')313314expect(screen.getByTestId('VideocamIcon')).toBeInTheDocument()315})316317it('does not render live view icon for node without VNC session', async () => {318render(319<MockedProvider mocks={mocks} addTypename={false}>320<Overview />321</MockedProvider>322)323324await screen.findByText('http://192.168.1.11:4444')325326const videocamIcons = screen.getAllByTestId('VideocamIcon')327328expect(videocamIcons.length).toBe(1)329330const node2Element = screen.getByText('http://192.168.1.11:4444')331const node2Card = node2Element.closest('.MuiCard-root')332333if (node2Card) {334expect(within(node2Card as HTMLElement).queryByTestId('VideocamIcon')).not.toBeInTheDocument()335}336})337338it('opens live view dialog when camera icon is clicked', async () => {339render(340<MockedProvider mocks={mocks} addTypename={false}>341<Overview />342</MockedProvider>343)344345await screen.findByText('http://192.168.1.10:4444')346347const user = userEvent.setup()348await user.click(screen.getByTestId('VideocamIcon'))349350expect(screen.getByText('Node Session Live View')).toBeInTheDocument()351expect(screen.getByTestId('mock-live-view')).toBeInTheDocument()352})353354it('closes live view dialog when close button is clicked', async () => {355render(356<MockedProvider mocks={mocks} addTypename={false}>357<Overview />358</MockedProvider>359)360361await screen.findByText('http://192.168.1.10:4444')362363const user = userEvent.setup()364await user.click(screen.getByTestId('VideocamIcon'))365366expect(screen.getByText('Node Session Live View')).toBeInTheDocument()367368await user.click(screen.getByRole('button', { name: /close/i }))369370expect(screen.queryByText('Node Session Live View')).not.toBeInTheDocument()371})372373it('handles error state', async () => {374const errorMocks = [375{376request: {377query: NODES_QUERY378},379error: new Error('Network error')380},381{382request: {383query: GRID_SESSIONS_QUERY384},385result: {386data: mockSessionsData387}388},389{390request: {391query: GRID_SESSIONS_QUERY392},393result: {394data: mockSessionsData395}396}397]398399render(400<MockedProvider mocks={errorMocks} addTypename={false}>401<Overview />402</MockedProvider>403)404405await new Promise(resolve => setTimeout(resolve, 0))406407const errorElement = screen.getByRole('heading', { level: 3 })408expect(errorElement).toBeInTheDocument()409})410411it('handles empty nodes state', async () => {412const emptyMocks = [413{414request: {415query: NODES_QUERY416},417result: {418data: { nodesInfo: { nodes: [] } }419}420},421{422request: {423query: GRID_SESSIONS_QUERY424},425result: {426data: mockSessionsData427}428},429{430request: {431query: GRID_SESSIONS_QUERY432},433result: {434data: mockSessionsData435}436}437]438439render(440<MockedProvider mocks={emptyMocks} addTypename={false}>441<Overview />442</MockedProvider>443)444445await screen.findByText('The Grid has no registered Nodes yet.')446447expect(screen.getByText('The Grid has no registered Nodes yet.')).toBeInTheDocument()448})449})450451452