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.