diff --git a/src/containers/Ledgers/LedgerMetrics.tsx b/src/containers/Ledgers/LedgerMetrics.tsx index ff2cf7a9c..80c677930 100644 --- a/src/containers/Ledgers/LedgerMetrics.tsx +++ b/src/containers/Ledgers/LedgerMetrics.tsx @@ -110,7 +110,7 @@ export const LedgerMetrics = ({ content = renderXRP(data[key], language) } else if (key === 'ledger_interval' && data[key] !== '--') { content = `${data[key]} ${t('seconds_short')}` - } else if (key === 'nUnl' && data[key]?.length === 0) { + } else if (key === 'nUnl' && (!data[key] || data[key]?.length === 0)) { return null } else if (key === 'nUnl') { content = data[key]?.length diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index 48f1e1a1c..f60c08143 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -19,11 +19,11 @@ export const Ledgers = ({ paused }: { paused: boolean }) => { ledgers, paused, ) - const isOnline = useIsOnline() + const { isOnline } = useIsOnline() return (
- {isOnline && ledgers ? ( + {isOnline && Object.keys(ledgers).length > 0 ? ( <>
diff --git a/src/containers/Ledgers/test/LedgersPage.test.js b/src/containers/Ledgers/test/LedgersPage.test.js index 3e946dea9..7645a5699 100644 --- a/src/containers/Ledgers/test/LedgersPage.test.js +++ b/src/containers/Ledgers/test/LedgersPage.test.js @@ -147,11 +147,12 @@ describe('Ledgers Page container', () => { expect(wrapper.find('.txn').length).toBe(0) server.send(prevLedgerMessage) + await sleep(260) wrapper.update() expect(wrapper.exists()).toBe(true) expect(wrapper.find('.ledgers').length).toBe(1) expect(wrapper.find('.ledger-list').length).toBe(1) - expect(wrapper.find('.ledger').length).toBe(1) + expect(wrapper.find('.ledger').length).toBe(2) server.send(validationMessage) // validation-messages are processed in batches every 200 ms diff --git a/src/containers/Network/Validators.tsx b/src/containers/Network/Validators.tsx index 5f2a10841..21d307bc2 100644 --- a/src/containers/Network/Validators.tsx +++ b/src/containers/Network/Validators.tsx @@ -36,42 +36,30 @@ export const ValidatorsData = () => { const [feeSettings, setFeeSettings] = useState() const merged = useMemo(() => { + const fromVHS = validatorsFromVHS ?? {} + const fromStream = validatorsFromValidations ?? {} + if ( - !validatorsFromVHS || - !( - validatorsFromValidations && - Object.keys(validatorsFromValidations).length - ) + Object.keys(fromVHS).length === 0 && + Object.keys(fromStream).length === 0 ) { return [] } const updated: Record = {} - const keys = new Set( - Object.keys(validatorsFromVHS).concat( - Object.keys(validatorsFromValidations), - ), - ) + const keys = new Set(Object.keys(fromVHS).concat(Object.keys(fromStream))) keys.forEach((d: string) => { - const newData: StreamValidator = - validatorsFromVHS[d] || validatorsFromValidations[d] + const newData: StreamValidator = fromVHS[d] ?? fromStream[d] if ( newData.ledger_index == null && - validatorsFromValidations[d] && - validatorsFromValidations[d].ledger_index + fromStream[d] && + fromStream[d].ledger_index ) { // VHS uses `current_index` instead of `ledger_index` // If `ledger_index` isn't defined, then we're still using the VHS data, // instead of the Streams data - newData.ledger_index = validatorsFromValidations[d].ledger_index - } - if (newData.current_index == null) { - newData.signing_key = validatorsFromValidations[d].signing_key - } - // latest hash and time comes from the validations stream - if (validatorsFromValidations[d]) { - newData.time = validatorsFromValidations[d].time - newData.ledger_hash = validatorsFromValidations[d].ledger_hash + newData.ledger_index = fromStream[d].ledger_index + newData.ledger_hash = fromStream[d].ledger_hash } updated[d] = newData diff --git a/src/containers/Network/test/validators.test.js b/src/containers/Network/test/validators.test.js index 1a1d4e1b1..57c6c2d41 100644 --- a/src/containers/Network/test/validators.test.js +++ b/src/containers/Network/test/validators.test.js @@ -7,34 +7,29 @@ import i18n from '../../../i18n/testConfig' import mockValidators from './mockValidators.json' import validationMessage from './mockValidation.json' import SocketContext from '../../shared/SocketContext' +import NetworkContext from '../../shared/NetworkContext' import MockWsClient from '../../test/mockWsClient' import { QuickHarness } from '../../test/utils' import { VALIDATORS_ROUTE } from '../../App/routes' import { Validators } from '../Validators' -import { StreamsProvider } from '../../shared/components/Streams' import { queryClient } from '../../shared/QueryClient' -import { VHSValidatorsProvider } from '../../shared/components/VHSValidators' const WS_URL = 'ws://localhost:1234' describe('Validators Tab container', () => { let server let client + const createWrapper = () => mount( - - - - - } /> - - - - + + + + } /> + + + , ) @@ -57,43 +52,75 @@ describe('Validators Tab container', () => { wrapper.unmount() }) - it('receives live validation', async () => { + it('displays validators from VHS API', (done) => { + moxios.stubRequest(`${process.env.VITE_DATA_URL}/validators/main`, { + status: 200, + response: { validators: mockValidators }, + }) + const wrapper = createWrapper() + moxios.wait(() => { + setTimeout(() => { + wrapper.update() + + // Should show 4 validators from VHS with 2 in UNL + expect(wrapper.find('.stat').text()).toEqual( + 'validators_found: 4 (unl: 2)', + ) + + // Should render hexagons visualization + expect(wrapper.find('.hexagons').length).toBe(1) + + wrapper.unmount() + done() + }, 100) + }) + }) + + it('merges validators from both VHS API and WebSocket stream', (done) => { + // Mock the VHS API response with 4 validators moxios.stubRequest(`${process.env.VITE_DATA_URL}/validators/main`, { status: 200, - response: mockValidators, + response: { validators: mockValidators }, }) - expect(wrapper.find('.validators').length).toBe(1) - expect(wrapper.find('.stat').html()).toBe( - '
validators_found: 0 (unl: )
', - ) - expect(wrapper.find('.validators-table').length).toBe(1) + const wrapper = createWrapper() + // Send a live validation message via WebSocket + // This validator (n9KaxgJv69FucW5kkiaMhCqS6sAR1wUVxpZaZmLGVXxAcAse9YhR) is NOT in mockValidators server.send(validationMessage) - setTimeout(() => { - wrapper.update() - expect(wrapper.find('.stat').html()).toBe( - '
validators_found: 4 (unl: 2)
', - ) - expect(wrapper.find('.validators .tooltip').length).toBe(0) - - wrapper.find('.validators .hexagon').first().simulate('mouseOver') - wrapper.update() - expect(wrapper.find('.validators-container .tooltip').length).toBe(1) - expect( - wrapper.find('.validators-container .tooltip .pubkey').html(), - ).toBe( - '
n9KaxgJv69FucW5kkiaMhCqS6sAR1wUVxpZaZmLGVXxAcAse9YhR
', - ) - wrapper.find('.validators .hexagon').first().simulate('mouseLeave') - wrapper.update() - expect(wrapper.find('.validators-container .tooltip').length).toBe(0) - expect(wrapper.find('.validators .hexagons').length).toBe(1) - expect(wrapper.find('.validators-table table tr').length).toBe(5) - wrapper.unmount() + // Wait for both VHS API and WebSocket data to be processed + moxios.wait(() => { + setTimeout(() => { + wrapper.update() + + // Should show 5 validators total (4 from VHS + 1 new from WebSocket) + // Note: The validation_public_key in mockValidation.json is different from all signing_keys in mockValidators.json + expect(wrapper.find('.stat').text()).toEqual( + 'validators_found: 5 (unl: 2)', + ) + + // Should render hexagons with merged data + expect(wrapper.find('.hexagons').length).toBe(1) + + // Should render validators table + expect(wrapper.find('.validators-table').length).toBe(1) + + // Check table has 5 data rows (4 from VHS + 1 from WebSocket) + const tableRows = wrapper.find('.validators-table table tbody tr') + expect(tableRows.length).toBe(5) + + // Verify that the WebSocket validator is in the table + const tableText = wrapper.find('.validators-table').text() + expect(tableText).toContain( + 'n9KaxgJv69FucW5kkiaMhCqS6sAR1wUVxpZaZmLGVXxAcAse9YhR', + ) + + wrapper.unmount() + done() + }, 100) }) }) }) diff --git a/src/containers/shared/components/Streams/StreamsProvider.tsx b/src/containers/shared/components/Streams/StreamsProvider.tsx index d675381b7..8367d439a 100644 --- a/src/containers/shared/components/Streams/StreamsProvider.tsx +++ b/src/containers/shared/components/Streams/StreamsProvider.tsx @@ -55,7 +55,7 @@ const truncateLedgers = (ledgers: Record, count) => export const StreamsProvider: FC = ({ children }) => { // In custom mode we populate metrics from ledgers loaded into memory const useServerMetrics = process.env.VITE_ENVIRONMENT !== 'custom' - const [ledgers, setLedgers] = useState>([]) + const [ledgers, setLedgers] = useState>({}) const ledgersRef = useRef>(ledgers) const firstLedgerRef = useRef(0) const [validators, setValidators] = useState>( @@ -78,9 +78,7 @@ export const StreamsProvider: FC = ({ children }) => { { enabled: socket.getState().online }, ) const { data: serverRunningMetrics, refetch: refetchServerRunningMetrics } = - useQuery('runningMetrics', () => fetchMetrics(), { - enabled: socket.getState().online, - }) + useQuery('runningMetrics', () => fetchMetrics()) function addLedger(index: number | string) { // Only add new ledgers that are newer than the last one added.