Repository: SiaFoundation/web
Pull request #1027: test(explorer): use cluster for testing
Author: login: telestrial
Created: 2025-04-02T02:46:26Z | Merged: 2025-04-04T19:47:30Z



Diff:
diff --git a/apps/explorer-e2e/src/fixtures/constants.ts b/apps/explorer-e2e/src/fixtures/constants.ts
index 65a0d3991..296fc36b5 100644
--- a/apps/explorer-e2e/src/fixtures/constants.ts
+++ b/apps/explorer-e2e/src/fixtures/constants.ts
@@ -1,3 +1,8 @@
+// Clusterd
+export const hostdCountDefault = 3
+export const renterdCountDefault = 1
+export const exploredCountDefault = 1
+
 // General/Shared
 export const SEARCHBAR = `input[name="query"]`
 export const APP_NAME = 'explorer-identity-appName'
@@ -8,6 +13,8 @@ export const DATUM_VALUE = 'explorer-datum-value'
 export const METRICS_ITEM = 'explorer-metrics-item'
 export const LATEST_BLOCKS_ITEM = 'explorer-latestBlocks-item'
 export const TOP_HOSTS_ITEMS = 'explorer-topHosts-item'
+export const HOME_ACTIVE_HOSTS = 'explorer-home-activeHosts'
+export const HOME_HEIGHT = 'explorer-home-height'
 
 // Contract page
 export const RENEWED_FROM_BUTTON = 'explorer-contract-renewedFrom'
@@ -15,69 +22,3 @@ export const RENEWED_TO_BUTTON = 'explorer-contract-renewedTo'
 
 // Host page
 export const HOST_PRICING = 'explorer-hostPricing'
-export const HOST_SETTINGS_PATTERNS = [
-  '\\b\\d+(?:\\.\\d{1,2})?\\s?(TB|GB|MB|KB|B)\\b', // 3.00 TB
-  '(\\$\\d+(?:\\.\\d{1,3})?|\\d+(?:\\.\\d{1,3})?\\s?[A-Z]{2})\\/(TB|GB|MB|KB|B)\\/(month|year|day)', // $1.16/TB/month
-  '(\\$\\d+(?:\\.\\d{1,3})?|\\d+(?:\\.\\d{1,3})?\\s?[A-Z]{2})\\/(TB|GB|MB|KB|B)', // $1.12/TB
-  '\\b\\d+\\s?(days?|months?|years?)\\b', // 6 months
-  '^\\$\\d+(?:\\.\\d+)?$', // $4.50
-  '(\\$\\d+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?\\s?[A-Z]{2})\\/million', // $0.0045 SC/million
-  '^(\\d+(?:\\.\\d+)?|0)\\s?(KS|mS|SC)$', // 1.000 KS, 200.000 mS, 0 SC
-  '\\b\\d+\\s?blocks\\b', // 25920 blocks
-  '^(6553[0-5]|(655[0-2][0-9]|64[0-9]{3}|6[0-3][0-9]{2}|[1-5]?[0-9]{0,4}))$', // 9883
-  '^\\d{1,16}$', // 2592000000000000
-  '\\b\\d+(?:\\.\\d{1,2})?\\s?(Mbps|Gbps|Kbps)\\b', // 998.64 Mbps
-]
-
-// Test data
-export const TEST_BLOCK_1 = {
-  height: '25000',
-  id: '0000000016920b69bfbe192005cfed7c9c5369bb83ff8406320f77f74f7b40cb',
-  display: {
-    title: 'Block 25,000',
-    blockHash: '000000...7b40cb',
-    minerPayoutAddress: '13caad1b4eea...',
-    transactionHeader: '2 transactions',
-    lastTransactionID: '8a03682f2285...',
-  },
-}
-
-export const TEST_TX_1 = {
-  id: '8a03682f22857f306a95f55a28fa9edf111e1d11803f8b4a91bd01d2803a4eb6',
-  display: {
-    title: 'Transaction 8a03682f22857f3...',
-    confirmationHeight: '25,000',
-    numberOfConfirmations: '72+ confirmations',
-    inputAddress: '83cc8e810db6...',
-    outputAddress: 'e3ed23ed389f...',
-    inputAmount: '+8.979 KS',
-    outputAmount: '+107.674 SC',
-    contractID: '25c94822bf7b...',
-  },
-}
-
-export const TEST_CONTRACT_1 = {
-  id: '25c94822bf7bd86a92d28a148d9d30151949f3599bf93af0df7b4f1e1b3c990d',
-  renewedFromTitle: 'Contract 494d147a8028217...',
-  renewedToTitle: 'Contract 43a81d1a21ebf6f...',
-  display: {
-    title: 'Contract 25c94822bf7bd86...',
-    transactionID: 'a3a4a6808e33...',
-    unlockHash: 'c50b70e7dd79...',
-    revisionNumber: '18,446,744,0...',
-    status: 'complete',
-    missedProofTitle: 'Missed proof outputs (3)',
-    validProofTitle: 'Valid proof outputs (2)',
-  },
-}
-
-export const TEST_HOST_1 = {
-  pubkey:
-    'ed25519:3926a0434232bba9eaca2041303a1039d4f65bf54d7bd4e2a9164ea2d778b714',
-  display: {
-    pubkey: 'ed25519:3926a0434232bba9eaca20...',
-    v1Title: '51.81.242.140:9882',
-    v2Title: '51.81.242.140:9884',
-    location: 'US',
-  },
-}
diff --git a/apps/explorer-e2e/src/helpers/exploredStabilization.ts b/apps/explorer-e2e/src/helpers/exploredStabilization.ts
new file mode 100644
index 000000000..32c6a55c0
--- /dev/null
+++ b/apps/explorer-e2e/src/helpers/exploredStabilization.ts
@@ -0,0 +1,47 @@
+import { Cluster } from '../fixtures/cluster'
+
+export async function exploredStabilization(
+  api: Cluster,
+  maxRetries = 20,
+  delayMs = 500,
+  stabilizationMs = 2000
+) {
+  for (let i = 0; i < maxRetries; i++) {
+    const tip = await api.daemons.explored.api.explorerTip()
+    const tipHeight = tip.data.height
+
+    console.log(
+      `observed explored tip height: ${tipHeight} — entering stabilization window`
+    )
+
+    const start = Date.now()
+    let isStable = true
+
+    while (Date.now() - start < stabilizationMs) {
+      const newTip = await api.daemons.explored.api.explorerTip()
+      const observedHeight = newTip.data.height
+
+      if (observedHeight !== tipHeight) {
+        console.log(
+          `explored height changed during stabilization — expected ${tipHeight}, got ${observedHeight}`
+        )
+        isStable = false
+        break
+      }
+
+      await new Promise((res) => setTimeout(res, 200))
+    }
+
+    if (isStable) {
+      console.log(`explored height stabilized at ${tipHeight}`)
+      return
+    }
+
+    console.log(`explored height did not stabilize — retrying...`)
+    await new Promise((res) => setTimeout(res, delayMs))
+  }
+
+  throw new Error(
+    `explored failed to stabilize height within ${maxRetries} retries`
+  )
+}
diff --git a/apps/explorer-e2e/src/specs/address.spec.ts b/apps/explorer-e2e/src/specs/address.spec.ts
index 6842a7400..8d6c83683 100644
--- a/apps/explorer-e2e/src/specs/address.spec.ts
+++ b/apps/explorer-e2e/src/specs/address.spec.ts
@@ -9,21 +9,20 @@ import {
 import { addWalletToWalletd, sendSiacoinFromRenterd } from '../fixtures/walletd'
 import { Cluster } from '../fixtures/cluster'
 import { toHastings } from '@siafoundation/units'
+import { exploredStabilization } from '../helpers/exploredStabilization'
 
 let explorerApp: ExplorerApp
 let cluster: Cluster
 
 test.beforeEach(async ({ page, context }) => {
-  // Start the cluster which includes an explored node, a renterd node, a
-  // walletd node, and 3 hostd nodes.
   cluster = await startCluster({ context })
-  // The renterd node will automatically form contracts with the hostd nodes.
-  // Wait for the contracts to form so that all related activity is on the
-  // blockchain and visible to explored.
   await renterdWaitForContracts({
     renterdNode: cluster.daemons.renterds[0].node,
     hostdCount: cluster.daemons.hostds.length,
   })
+
+  await exploredStabilization(cluster)
+
   explorerApp = new ExplorerApp(page)
 })
 
@@ -98,5 +97,7 @@ test('address can navigate through to a transaction', async ({ page }) => {
     .locator(`a[data-testid="entity-link"][href*="${events.data[0].id}"]`)
     .click()
 
-  await expect(page.getByText(events.data[0].id.slice(0, 5))).toBeVisible()
+  await expect(
+    page.getByTestId('entity-heading').getByText(events.data[0].id.slice(0, 5))
+  ).toBeVisible()
 })
diff --git a/apps/explorer-e2e/src/specs/block.spec.ts b/apps/explorer-e2e/src/specs/block.spec.ts
index dea182f37..0f31f89c3 100644
--- a/apps/explorer-e2e/src/specs/block.spec.ts
+++ b/apps/explorer-e2e/src/specs/block.spec.ts
@@ -1,70 +1,129 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
-import { TEST_BLOCK_1 } from '../fixtures/constants'
-import { keys } from '../utils'
+import { Cluster, startCluster } from '../fixtures/cluster'
+import {
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import { exploredStabilization } from '../helpers/exploredStabilization'
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
+
+  await exploredStabilization(cluster)
 
-test.beforeEach(async ({ page }) => {
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
+  await teardownCluster()
+})
+
 test('block can be searched by height', async ({ page }) => {
+  const { data } = await cluster.daemons.hostds[0].api.consensusTip()
+  const testBlock = String(data.height - 1) // Minus one to avoid home page test success.
+
   await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_BLOCK_1.height)
+  await explorerApp.navigateBySearchBar(testBlock)
 
-  await expect(page.getByText(TEST_BLOCK_1.display.title).nth(0)).toBeVisible()
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Block ' + testBlock.toLocaleString())
+  ).toBeVisible()
 })
 
 test('block can be searched by id', async ({ page }) => {
+  const { data } = await cluster.daemons.hostds[0].api.consensusTip()
+  const blockID = String(data.id)
+
   await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_BLOCK_1.id)
+  await explorerApp.navigateBySearchBar(blockID)
 
-  await expect(page.getByText(TEST_BLOCK_1.display.title).nth(0)).toBeVisible()
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Block ' + data.height.toLocaleString())
+  ).toBeVisible()
 })
 
 test('block can be directly navigated to by height', async ({ page }) => {
-  await explorerApp.goTo('/block/' + TEST_BLOCK_1.height)
+  const { data } = await cluster.daemons.hostds[0].api.consensusTip()
+  const height = String(data.height - 1) // Minus one to avoid home page test success.
 
-  await expect(page.getByText(TEST_BLOCK_1.display.title).nth(0)).toBeVisible()
+  await explorerApp.goTo('/block/' + height)
+
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Block ' + height.toLocaleString())
+  ).toBeVisible()
+})
+
+test('block can be directly navigated to by id', async ({ page }) => {
+  const { data } = await cluster.daemons.hostds[0].api.consensusTip()
+  const blockID = String(data.id)
+
+  await explorerApp.goTo('/block/' + blockID)
+
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Block ' + data.height.toLocaleString())
+  ).toBeVisible()
 })
 
 test('block can navigate to previous block', async ({ page }) => {
-  await explorerApp.goTo('/block/' + TEST_BLOCK_1.id)
+  const { data } = await cluster.daemons.hostds[0].api.consensusTip()
+  const height = String(data.height - 1) // Minus one to avoid home page test success.
+
+  await explorerApp.goTo('/block/' + height)
   await page.getByTestId('explorer-block-prevBlock').click()
 
   await expect(
-    page.getByText((Number(TEST_BLOCK_1.height) - 1).toLocaleString())
+    page
+      .getByTestId('entity-heading')
+      .getByText((Number(height) - 1).toLocaleString())
   ).toBeVisible()
 })
 
 test('block can navigate to nextblock', async ({ page }) => {
-  await explorerApp.goTo('/block/' + TEST_BLOCK_1.id)
+  const { data } = await cluster.daemons.hostds[0].api.consensusTip()
+  const height = String(data.height - 1) // Minus one to avoid home page test success.
+
+  await explorerApp.goTo('/block/' + height)
   await page.getByTestId('explorer-block-nextBlock').click()
 
   await expect(
-    page.getByText((Number(TEST_BLOCK_1.height) + 1).toLocaleString())
+    page
+      .getByTestId('entity-heading')
+      .getByText((Number(height) + 1).toLocaleString())
   ).toBeVisible()
 })
 
 test('block can click through to a transaction', async ({ page }) => {
-  await explorerApp.goTo('/block/' + TEST_BLOCK_1.id)
-  await page
-    .locator(
-      'a[data-testid="entity-link"][href*="8a03682f22857f306a95f55a28fa9edf"]'
-    )
-    .click()
-
-  await expect(page.getByText('Transaction 8a03682f22857f3...')).toBeVisible()
-})
+  const events = await cluster.daemons.renterds[0].api.walletEvents({
+    params: {
+      limit: 1,
+      offset: 0,
+    },
+  })
+  const transaction = events.data[0]
 
-test('block displays the intended data', async ({ page }) => {
-  const displayKeys = keys(TEST_BLOCK_1.display)
+  await explorerApp.goTo('/block/' + transaction.maturityHeight)
 
-  await explorerApp.goTo('/block/' + TEST_BLOCK_1.height)
+  await page
+    .locator(`a[data-testid="entity-link"][href*="${transaction.id}"]`)
+    .click()
 
-  for (const key of displayKeys) {
-    const currentProperty = TEST_BLOCK_1.display[key]
-    await expect(page.getByText(currentProperty)).toBeVisible()
-  }
+  await expect(
+    page.getByTestId('entity-heading').getByText(transaction.id.slice(0, 5))
+  ).toBeVisible()
 })
diff --git a/apps/explorer-e2e/src/specs/contract.spec.ts b/apps/explorer-e2e/src/specs/contract.spec.ts
index 8b2d494d9..c7f527e0b 100644
--- a/apps/explorer-e2e/src/specs/contract.spec.ts
+++ b/apps/explorer-e2e/src/specs/contract.spec.ts
@@ -1,52 +1,60 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import { exploredStabilization } from '../helpers/exploredStabilization'
+// import { RENEWED_FROM_BUTTON,
+//  RENEWED_TO_BUTTON,} from '../fixtures/constants'
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
 
-test.beforeEach(async ({ page }) => {
-  explorerApp = new ExplorerApp(page)
-})
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
 
-test('contract can be searched by id', async ({ page }) => {
-  await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_CONTRACT_1.id)
+  await exploredStabilization(cluster)
 
-  await expect(page.getByText(TEST_CONTRACT_1.display.title)).toBeVisible()
+  explorerApp = new ExplorerApp(page)
 })
 
-test('contract can be directly navigated to', async ({ page }) => {
-  await explorerApp.goTo('/contract/' + TEST_CONTRACT_1.id)
-
-  await expect(page.getByText(TEST_CONTRACT_1.display.title)).toBeVisible()
+test.afterEach(async () => {
+  await teardownCluster()
 })
 
-test('contract displays the intended data', async ({ page }) => {
-  const displayKeys = keys(TEST_CONTRACT_1.display)
+test('contract can be searched by id', async ({ page }) => {
+  const hostContracts = await cluster.daemons.hostds[0].api.contracts({
+    data: { statuses: ['active'] },
+  })
+  const contractID = hostContracts.data.contracts[0].revision.parentID
 
-  await explorerApp.goTo('/contract/' + TEST_CONTRACT_1.id)
+  await explorerApp.goTo('/')
+  await explorerApp.navigateBySearchBar(contractID)
 
-  for (const key of displayKeys) {
-    const currentProperty = TEST_CONTRACT_1.display[key]
-    await expect(page.getByText(currentProperty)).toBeVisible()
-  }
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Contract ' + contractID.slice(0, 5))
+  ).toBeVisible()
 })
 
-// test('contract can navigate to renewed from contract', async ({ page }) => {
-//   await explorerApp.goTo('/contract/' + TEST_CONTRACT_1.id)
-//   await page.getByTestId(RENEWED_FROM_BUTTON).click()
-
-//   await expect(page.getByText(TEST_CONTRACT_1.renewedFromTitle)).toBeVisible()
-// })
-
-// test('contract can navigate to renewed to contract', async ({ page }) => {
-//   await explorerApp.goTo('/contract/' + TEST_CONTRACT_1.id)
-//   await page.getByTestId(RENEWED_TO_BUTTON).click()
-
-//   await expect(page.getByText(TEST_CONTRACT_1.renewedToTitle)).toBeVisible()
-// })
+test('contract can be directly navigated to', async ({ page }) => {
+  const hostContracts = await cluster.daemons.hostds[0].api.contracts({
+    data: { statuses: ['active'] },
+  })
+  const contractID = hostContracts.data.contracts[0].revision.parentID
+
+  await explorerApp.goTo('/contract/' + contractID)
+
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Contract ' + contractID.slice(0, 5))
+  ).toBeVisible()
+})
diff --git a/apps/explorer-e2e/src/specs/home.spec.ts b/apps/explorer-e2e/src/specs/home.spec.ts
index b16bf8ed0..d05408280 100644
--- a/apps/explorer-e2e/src/specs/home.spec.ts
+++ b/apps/explorer-e2e/src/specs/home.spec.ts
@@ -3,27 +3,38 @@ import { ExplorerApp } from '../fixtures/ExplorerApp'
 import {
   APP_NAME,
   APP_NETWORK,
+  HOME_ACTIVE_HOSTS,
+  HOME_HEIGHT,
   LATEST_BLOCKS_ITEM,
   METRICS_ITEM,
   TOP_HOSTS_ITEMS,
 } from '../fixtures/constants'
+import { Cluster, startCluster } from '../fixtures/cluster'
+import { teardownCluster } from '@siafoundation/clusterd'
+import { exploredStabilization } from '../helpers/exploredStabilization'
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await exploredStabilization(cluster)
 
-test.beforeEach(async ({ page }) => {
   explorerApp = new ExplorerApp(page)
 
+  // All these tests will start and end on index.
   await explorerApp.goTo('/')
 })
 
+test.afterEach(async () => {
+  await teardownCluster()
+})
+
 test('home displays app identity', async ({ page }) => {
   await expect(page.getByTestId(APP_NAME)).toContainText('siascan')
-  await expect(page.getByTestId(APP_NETWORK)).toContainText('zen') // Can we get this via environmental variables? Will we always run these on zen?
+  await expect(page.getByTestId(APP_NETWORK)).toContainText('zen')
 })
 
-// The expectations below have their boundary around the display of a sucessful network call.
-// If the call fails, we fail, but if the call's contents change, we do not fail, as long as
-// we display something.
 test('home displays metrics', async ({ page }) => {
   expect((await page.getByTestId(METRICS_ITEM).count()) > 0).toBeTruthy()
 })
@@ -35,3 +46,15 @@ test('home displays latest blocks', async ({ page }) => {
 test('home displays top hosts', async ({ page }) => {
   expect((await page.getByTestId(TOP_HOSTS_ITEMS).count()) > 0).toBeTruthy()
 })
+
+test('home displays accurate network information', async ({ page }) => {
+  const activeHostCount = cluster.daemons.hostds.length
+  const {
+    data: { height },
+  } = await cluster.daemons.hostds[0].api.consensusTip()
+
+  await expect(page.getByTestId(HOME_ACTIVE_HOSTS)).toContainText(
+    String(activeHostCount)
+  )
+  await expect(page.getByTestId(HOME_HEIGHT)).toContainText(String(height))
+})
diff --git a/apps/explorer-e2e/src/specs/host.spec.ts b/apps/explorer-e2e/src/specs/host.spec.ts
index 6870efd76..4d1cebb46 100644
--- a/apps/explorer-e2e/src/specs/host.spec.ts
+++ b/apps/explorer-e2e/src/specs/host.spec.ts
@@ -1,68 +1,81 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
+import { teardownCluster } from '@siafoundation/clusterd'
+import { exploredStabilization } from '../helpers/exploredStabilization'
 import {
-  DATUM_VALUE,
-  TEST_HOST_1,
-  HOST_SETTINGS_PATTERNS,
-  HOST_PRICING,
-} from '../fixtures/constants'
+  getDownloadCost,
+  getStorageCost,
+  getUploadCost,
+} from '@siafoundation/units'
+import { currencyOptions } from '@siafoundation/react-core'
 
 // Hosts can not be guaranteed like other test slices. Therefore, we'll grab
 // the displayed values and test them against the patterns we expect to see.
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await exploredStabilization(cluster)
 
-test.beforeEach(async ({ page }) => {
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
+  await teardownCluster()
+})
+
 test('host can be searched by id', async ({ page }) => {
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+
   await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_HOST_1.pubkey)
+  await explorerApp.navigateBySearchBar(publicKey)
 
   await expect(
-    page
-      .getByText(TEST_HOST_1.display.v1Title)
-      .or(page.getByText(TEST_HOST_1.display.v2Title))
+    page.getByText(String(publicKey.slice(0, 6))).first()
   ).toBeVisible()
 })
 
 test('host can be directly navigated to', async ({ page }) => {
-  await explorerApp.goTo('/host/' + TEST_HOST_1.pubkey)
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+
+  await explorerApp.goTo('/host/' + publicKey)
 
   await expect(
-    page
-      .getByText(TEST_HOST_1.display.v1Title)
-      .or(page.getByText(TEST_HOST_1.display.v2Title))
+    page.getByText(String(publicKey.slice(0, 6))).first()
   ).toBeVisible()
 })
 
-test('host displays properly formatted host pricing', async ({ page }) => {
-  await explorerApp.goTo('/host/' + TEST_HOST_1.pubkey)
-
-  const hostSettings = await page.getByTestId(HOST_PRICING).allInnerTexts()
-
-  for (const text of hostSettings) {
-    const matched = HOST_SETTINGS_PATTERNS.some((pattern) => {
-      return text.match(new RegExp(pattern))
-    })
-    // If we're failing, uncomment this and check console.
-    // console.log(matched || text)
-    expect(matched).toBe(true)
-  }
-})
-
-test('host displays properly formatted host settings', async ({ page }) => {
-  await explorerApp.goTo('/host/' + TEST_HOST_1.pubkey)
+test('host displays properly formatted data', async ({ page }) => {
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+  const usd = currencyOptions.filter((currency) => currency.id === 'usd')[0]
+  const {
+    data: { egressPrice, ingressPrice, storagePrice },
+  } = await cluster.daemons.hostds[0].api.settings()
+  const { data: rate } = await cluster.daemons.explored.api.exchangeRate({
+    params: { currency: usd.id },
+  })
+  const downloadCost = getDownloadCost({
+    price: egressPrice,
+    exchange: { currency: { prefix: usd.prefix }, rate: rate.toString() },
+  })
+  const uploadCost = getUploadCost({
+    price: ingressPrice,
+    exchange: { currency: { prefix: usd.prefix }, rate: rate.toString() },
+  })
+  const storageCost = getStorageCost({
+    price: storagePrice,
+    exchange: { currency: { prefix: usd.prefix }, rate: rate.toString() },
+  })
 
-  const hostSettings = await page.getByTestId(DATUM_VALUE).allInnerTexts()
+  await explorerApp.goTo('/host/' + publicKey)
 
-  for (const text of hostSettings) {
-    const matched = HOST_SETTINGS_PATTERNS.some((pattern) => {
-      return text.match(new RegExp(pattern))
-    })
-    // If we're failing, uncomment this and check console.
-    // console.log(matched || text)
-    expect(matched).toBe(true)
-  }
+  expect(await page.getByText(downloadCost).count()).toBe(2)
+  expect(await page.getByText(uploadCost).count()).toBe(2)
+  expect(await page.getByText(storageCost).count()).toBe(2)
 })
diff --git a/apps/explorer-e2e/src/specs/tx.spec.ts b/apps/explorer-e2e/src/specs/tx.spec.ts
index 3b8d57323..528504b9f 100644
--- a/apps/explorer-e2e/src/specs/tx.spec.ts
+++ b/apps/explorer-e2e/src/specs/tx.spec.ts
@@ -1,57 +1,98 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
-import { TEST_TX_1 } from '../fixtures/constants'
-import { keys } from '../utils'
+import { Cluster, startCluster } from '../fixtures/cluster'
+import {
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import { exploredStabilization } from '../helpers/exploredStabilization'
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
+
+  await exploredStabilization(cluster)
 
-test.beforeEach(async ({ page }) => {
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
+  await teardownCluster()
+})
+
 test('transaction can be searched by id', async ({ page }) => {
+  const events = await cluster.daemons.renterds[0].api.walletEvents({
+    params: {
+      limit: 1,
+      offset: 0,
+    },
+  })
+  const transactionID = events.data[0].id
+
   await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_TX_1.id)
+  await explorerApp.navigateBySearchBar(transactionID)
 
-  await expect(page.getByText(TEST_TX_1.display.title)).toBeVisible()
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Transaction ' + transactionID.slice(0, 5))
+  ).toBeVisible()
 })
 
 test('transaction can be navigated to by id', async ({ page }) => {
-  await explorerApp.goTo('/tx/' + TEST_TX_1.id)
+  const events = await cluster.daemons.renterds[0].api.walletEvents({
+    params: {
+      limit: 1,
+      offset: 0,
+    },
+  })
+  const transactionID = events.data[0].id
 
-  await expect(page.getByText(TEST_TX_1.display.title)).toBeVisible()
+  await explorerApp.goTo('/tx/' + transactionID)
+
+  await expect(
+    page
+      .getByTestId('entity-heading')
+      .getByText('Transaction ' + transactionID.slice(0, 5))
+  ).toBeVisible()
 })
 
 test('transaction can click through to a contract', async ({ page }) => {
-  await explorerApp.goTo('/tx/' + TEST_TX_1.id)
-  await page
-    .locator(
-      'a[data-testid="entity-link"][href*="25c94822bf7bd86a92d28a148d9d30151949f3599bf93af0df7b4f1e1b3c990d"]'
-    )
-    .click()
-
-  await expect(page.getByText('Contract 25c94822bf7bd86...')).toBeVisible()
-})
+  const events = await cluster.daemons.renterds[0].api.walletEvents({
+    params: {
+      limit: 1,
+      offset: 0,
+    },
+  })
+  const transactionID = events.data[0].id
 
-test('transaction can click through to an address', async ({ page }) => {
-  await explorerApp.goTo('/tx/' + TEST_TX_1.id)
-  await page
-    .locator(
-      'a[data-testid="entity-link"][href*="68bf48e81536f2221f3809aa9d1c89c1c869a17c6f186a088e49fd2605e4bfaaa24f26e4c42c"]'
-    )
-    .nth(0)
-    .click()
-
-  await expect(page.getByText('Address 68bf48e81536f22...')).toBeVisible()
+  await explorerApp.goTo('/tx/' + transactionID)
+  await page.getByRole('link', { name: 'C', exact: true }).click()
+
+  await expect(
+    page.getByTestId('entity-heading').getByText('Contract')
+  ).toBeVisible()
 })
 
-test('transaction displays the intended data', async ({ page }) => {
-  const displayKeys = keys(TEST_TX_1.display)
+test('transaction can click through to an address', async ({ page }) => {
+  const events = await cluster.daemons.renterds[0].api.walletEvents({
+    params: {
+      limit: 1,
+      offset: 0,
+    },
+  })
+  const transactionID = events.data[0].id
 
-  await explorerApp.goTo('/tx/' + TEST_TX_1.id)
+  await explorerApp.goTo('/tx/' + transactionID)
+  await page.getByRole('link', { name: 'SO', exact: true }).first().click()
 
-  for (const key of displayKeys) {
-    const currentProperty = TEST_TX_1.display[key]
-    await expect(page.getByText(currentProperty)).toBeVisible()
-  }
+  await expect(
+    page.getByTestId('entity-heading').getByText('Address')
+  ).toBeVisible()
 })
diff --git a/apps/explorer-e2e/src/utils.ts b/apps/explorer-e2e/src/utils.ts
deleted file mode 100644
index 732e58d63..000000000
--- a/apps/explorer-e2e/src/utils.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Properly typed version of Object.keys.
-export function keys<T extends object>(obj: T) {
-  return Object.keys(obj) as Array<keyof T>
-}
diff --git a/apps/explorer/components/Home/index.tsx b/apps/explorer/components/Home/index.tsx
index 54022e1a0..10b24d39d 100644
--- a/apps/explorer/components/Home/index.tsx
+++ b/apps/explorer/components/Home/index.tsx
@@ -64,6 +64,7 @@ export function Home({
               className="text-xl md:text-3xl"
               weight="semibold"
               color="contrast"
+              data-testid="explorer-home-height"
             >
               {humanNumber(blockHeight)}
             </Text>
@@ -118,7 +119,10 @@ export function Home({
         {
           label: 'Active hosts',
           value: (
-            <div className="flex flex-col gap-1 items-baseline">
+            <div
+              className="flex flex-col gap-1 items-baseline"
+              data-testid="explorer-home-activeHosts"
+            >
               <Tooltip content="Active hosts">
                 <Text
                   className="text-xl md:text-3xl"


Reviews:
user: login: peterjan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T06:44:37Z
user: login: alexfreska
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T13:15:58Z
user: login: alexfreska
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T13:18:13Z
user: login: alexfreska
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T13:18:31Z
user: login: alexfreska
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T13:24:58Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T23:51:59Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T23:54:56Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T23:55:12Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T23:55:20Z
user: login: telestrial
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T00:17:38Z
user: login: peterjan
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T07:00:02Z
user: login: alexfreska
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T14:42:10Z
user: login: alexfreska
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T14:43:32Z

Review comments:
diff_hunk: @@ -1,52 +1,57 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import // RENEWED_FROM_BUTTON,
path: apps/explorer-e2e/src/specs/contract.spec.ts
user: login: peterjan
body: we can get rid of this `import` 
created_at: 2025-04-02T06:36:35Z
author_association: MEMBER
diff_hunk: @@ -1,52 +1,57 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import // RENEWED_FROM_BUTTON,
+// RENEWED_TO_BUTTON,
+'../fixtures/constants'
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
path: apps/explorer-e2e/src/specs/contract.spec.ts
user: login: peterjan
body: this is the same as `test.afterEach(teardownCluster)` right? been a while
created_at: 2025-04-02T06:37:22Z
author_association: MEMBER
diff_hunk: @@ -1,52 +1,57 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import // RENEWED_FROM_BUTTON,
+// RENEWED_TO_BUTTON,
+'../fixtures/constants'
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/contract.spec.ts
user: login: peterjan
body: we always repeat this step, perhaps we should consider making it part of `startCluster`, or pass in optional parameters that form n contracts and wait until they're formed
created_at: 2025-04-02T06:43:31Z
author_association: MEMBER
diff_hunk: @@ -1,68 +1,49 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  DATUM_VALUE,
-  TEST_HOST_1,
-  HOST_SETTINGS_PATTERNS,
-  HOST_PRICING,
-} from '../fixtures/constants'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
 
 // Hosts can not be guaranteed like other test slices. Therefore, we'll grab
 // the displayed values and test them against the patterns we expect to see.
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
+  await teardownCluster()
+})
+
 test('host can be searched by id', async ({ page }) => {
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+
   await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_HOST_1.pubkey)
+  await explorerApp.navigateBySearchBar(publicKey)
 
   await expect(
-    page
-      .getByText(TEST_HOST_1.display.v1Title)
-      .or(page.getByText(TEST_HOST_1.display.v2Title))
+    page.getByText(String(publicKey.slice(0, 6))).first()
   ).toBeVisible()
 })
 
 test('host can be directly navigated to', async ({ page }) => {
-  await explorerApp.goTo('/host/' + TEST_HOST_1.pubkey)
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+
+  await explorerApp.goTo('/host/' + publicKey)
 
   await expect(
-    page
-      .getByText(TEST_HOST_1.display.v1Title)
-      .or(page.getByText(TEST_HOST_1.display.v2Title))
+    page.getByText(String(publicKey.slice(0, 6))).first()
   ).toBeVisible()
 })
-
-test('host displays properly formatted host pricing', async ({ page }) => {
path: apps/explorer-e2e/src/specs/host.spec.ts
user: login: alexfreska
body: Replace this with a test that fetches the host settings and checks maybe the main pricing fields?
created_at: 2025-04-02T13:15:57Z
author_association: MEMBER
diff_hunk: @@ -3,27 +3,42 @@ import { ExplorerApp } from '../fixtures/ExplorerApp'
 import {
   APP_NAME,
   APP_NETWORK,
+  HOME_ACTIVE_HOSTS,
+  HOME_HEIGHT,
   LATEST_BLOCKS_ITEM,
   METRICS_ITEM,
   TOP_HOSTS_ITEMS,
 } from '../fixtures/constants'
+import { Cluster, startCluster } from '../fixtures/cluster'
+import {
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
 
-test.beforeEach(async ({ page }) => {
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/home.spec.ts
user: login: alexfreska
body: Do we need to wait for contracts for these tests?
created_at: 2025-04-02T13:18:12Z
author_association: MEMBER
diff_hunk: @@ -1,68 +1,49 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  DATUM_VALUE,
-  TEST_HOST_1,
-  HOST_SETTINGS_PATTERNS,
-  HOST_PRICING,
-} from '../fixtures/constants'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
 
 // Hosts can not be guaranteed like other test slices. Therefore, we'll grab
 // the displayed values and test them against the patterns we expect to see.
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/host.spec.ts
user: login: alexfreska
body: Do we need to wait for contracts for these tests?
created_at: 2025-04-02T13:18:30Z
author_association: MEMBER
diff_hunk: @@ -1,52 +1,57 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import // RENEWED_FROM_BUTTON,
+// RENEWED_TO_BUTTON,
+'../fixtures/constants'
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/contract.spec.ts
user: login: alexfreska
body: I think we can actually remove this from some of the other suites.
created_at: 2025-04-02T13:24:58Z
author_association: MEMBER
diff_hunk: @@ -1,52 +1,57 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import // RENEWED_FROM_BUTTON,
+// RENEWED_TO_BUTTON,
+'../fixtures/constants'
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/contract.spec.ts
user: login: telestrial
body: Correct. We can remove this other places. Not here, but in home and host.
created_at: 2025-04-03T23:51:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,52 +1,57 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  // RENEWED_FROM_BUTTON,
-  // RENEWED_TO_BUTTON,
-  TEST_CONTRACT_1,
-} from '../fixtures/constants'
-import { keys } from '../utils'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
+import // RENEWED_FROM_BUTTON,
+// RENEWED_TO_BUTTON,
+'../fixtures/constants'
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
path: apps/explorer-e2e/src/specs/contract.spec.ts
user: login: telestrial
body: We need to pass a function reference to `afterEach`, so it would not be the same here. `teardownCluster` is also async, so we should wait to make sure we're torn down before moving on.
created_at: 2025-04-03T23:54:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,27 +3,42 @@ import { ExplorerApp } from '../fixtures/ExplorerApp'
 import {
   APP_NAME,
   APP_NETWORK,
+  HOME_ACTIVE_HOSTS,
+  HOME_HEIGHT,
   LATEST_BLOCKS_ITEM,
   METRICS_ITEM,
   TOP_HOSTS_ITEMS,
 } from '../fixtures/constants'
+import { Cluster, startCluster } from '../fixtures/cluster'
+import {
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
 
 let explorerApp: ExplorerApp
+let cluster: Cluster
 
-test.beforeEach(async ({ page }) => {
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/home.spec.ts
user: login: telestrial
body: Nope. Removed!
created_at: 2025-04-03T23:55:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,68 +1,49 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  DATUM_VALUE,
-  TEST_HOST_1,
-  HOST_SETTINGS_PATTERNS,
-  HOST_PRICING,
-} from '../fixtures/constants'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
 
 // Hosts can not be guaranteed like other test slices. Therefore, we'll grab
 // the displayed values and test them against the patterns we expect to see.
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
path: apps/explorer-e2e/src/specs/host.spec.ts
user: login: telestrial
body: Nope. Removed!
created_at: 2025-04-03T23:55:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,68 +1,49 @@
 import { test, expect } from '@playwright/test'
 import { ExplorerApp } from '../fixtures/ExplorerApp'
+import { Cluster, startCluster } from '../fixtures/cluster'
 import {
-  DATUM_VALUE,
-  TEST_HOST_1,
-  HOST_SETTINGS_PATTERNS,
-  HOST_PRICING,
-} from '../fixtures/constants'
+  renterdWaitForContracts,
+  teardownCluster,
+} from '@siafoundation/clusterd'
 
 // Hosts can not be guaranteed like other test slices. Therefore, we'll grab
 // the displayed values and test them against the patterns we expect to see.
 
 let explorerApp: ExplorerApp
-
-test.beforeEach(async ({ page }) => {
+let cluster: Cluster
+
+test.beforeEach(async ({ page, context }) => {
+  cluster = await startCluster({ context })
+  await renterdWaitForContracts({
+    renterdNode: cluster.daemons.renterds[0].node,
+    hostdCount: cluster.daemons.hostds.length,
+  })
   explorerApp = new ExplorerApp(page)
 })
 
+test.afterEach(async () => {
+  await teardownCluster()
+})
+
 test('host can be searched by id', async ({ page }) => {
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+
   await explorerApp.goTo('/')
-  await explorerApp.navigateBySearchBar(TEST_HOST_1.pubkey)
+  await explorerApp.navigateBySearchBar(publicKey)
 
   await expect(
-    page
-      .getByText(TEST_HOST_1.display.v1Title)
-      .or(page.getByText(TEST_HOST_1.display.v2Title))
+    page.getByText(String(publicKey.slice(0, 6))).first()
   ).toBeVisible()
 })
 
 test('host can be directly navigated to', async ({ page }) => {
-  await explorerApp.goTo('/host/' + TEST_HOST_1.pubkey)
+  const host = await cluster.daemons.hostds[0].api.stateHost()
+  const { publicKey } = host.data
+
+  await explorerApp.goTo('/host/' + publicKey)
 
   await expect(
-    page
-      .getByText(TEST_HOST_1.display.v1Title)
-      .or(page.getByText(TEST_HOST_1.display.v2Title))
+    page.getByText(String(publicKey.slice(0, 6))).first()
   ).toBeVisible()
 })
-
-test('host displays properly formatted host pricing', async ({ page }) => {
path: apps/explorer-e2e/src/specs/host.spec.ts
user: login: telestrial
body: Got it. Thank you!
created_at: 2025-04-04T00:17:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,9 @@
+import Axios from 'axios'
+import { clusterd } from '@siafoundation/clusterd'
+
+export async function getExploredTip() {
+  const response = await Axios.get(
+    `http://localhost:${clusterd.managementPort}/nodes/proxy/explored/api/explorer/tip`
path: apps/explorer-e2e/src/helpers/getExploredTip.ts
user: login: alexfreska
body: This should be the same as using the existing sdk `cluster.daemons.explored.api.explorerTip()`
created_at: 2025-04-04T14:42:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+import { getExploredTip } from './getExploredTip'
+
+export async function exploredStabilization(
+  maxRetries = 20,
+  delayMs = 500,
+  stabilizationMs = 2000
+) {
+  for (let i = 0; i < maxRetries; i++) {
+    const tip = await getExploredTip()
+    const tipHeight = tip.data.height
+
+    console.log(
+      `observed explored tip height: ${tipHeight} — entering stabilization window`
+    )
+
+    const start = Date.now()
+    let isStable = true
+
+    while (Date.now() - start < stabilizationMs) {
+      const newTip = await getExploredTip()
+      const observedHeight = newTip.data.height
+
+      if (observedHeight !== tipHeight) {
+        console.log(
+          `explored height changed during stabilization — expected ${tipHeight}, got ${observedHeight}`
+        )
+        isStable = false
+        break
+      }
+
+      await new Promise((res) => setTimeout(res, 200))
+    }
+
+    if (isStable) {
+      console.log(`explored height stabilized at ${tipHeight}`)
path: apps/explorer-e2e/src/helpers/exploredStabilization.ts
user: login: alexfreska
body: Where did we land on this, yesterday it looked like the height was tracking the cluster height?
created_at: 2025-04-04T14:43:32Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-02T02:46:29Z
body: ###  ⚠️  No Changeset found

Latest commit: 988fabd26b5e59e9b22410faec9e4467933d1a39

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/SiaFoundation/web/new/02-26-test_explorer_use_cluster_for_testing?filename=.changeset/swift-seals-flow.md&value=---%0A%22explorer%22%3A%20patch%0A---%0A%0Atest(explorer)%3A%20use%20cluster%20for%20testing%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-02T02:46:29Z
body: [vc]: #a4KhEn1H1SHyOUfGDcSSoOPnHKbZx19tTi1FBhLCsdc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyZW50ZXJkIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NpYS1mb3VuZGF0aW9uL3JlbnRlcmQvNEc2SjU4MVRNelRXUW1IOE16YkZQQUZNSlRYaSIsInByZXZpZXdVcmwiOiJyZW50ZXJkLWdpdC0wMi0yNi10ZXN0ZXhwbG9yZXJ1c2VjbHVzdGVyZi1lYTNmMmEtc2lhLWZvdW5kYXRpb24udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fSx7Im5hbWUiOiJob3N0ZCIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zaWEtZm91bmRhdGlvbi9ob3N0ZC84VzdIRVVhdWtXZ0pKNGtGUUxiejZaZXgySnlFIiwicHJldmlld1VybCI6Imhvc3RkLWdpdC0wMi0yNi10ZXN0ZXhwbG9yZXJ1c2VjbHVzdGVyZm9ydGVzdGluZy1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6IndlYnNpdGUiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2lhLWZvdW5kYXRpb24vd2Vic2l0ZS9IazJCajhneHZMQmJHaHl4SzcxRHIzOXFkeEJRIiwicHJldmlld1VybCI6IndlYnNpdGUtZ2l0LTAyLTI2LXRlc3RleHBsb3JlcnVzZWNsdXN0ZXJmLTQ5YWZjOC1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6ImV4cGxvcmVyIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NpYS1mb3VuZGF0aW9uL2V4cGxvcmVyL0V6OVU3VDdnTnRITXRjamF0WXZ5dDJZQmtRYkYiLCJwcmV2aWV3VXJsIjoiZXhwbG9yZXItZ2l0LTAyLTI2LXRlc3RleHBsb3JlcnVzZWNsdXN0ZXItMzc2NmRkLXNpYS1mb3VuZGF0aW9uLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZXhwbG9yZXItZ2l0LTAyLTI2LXRlc3RleHBsb3JlcnVzZWNsdXN0ZXItMzc2NmRkLXNpYS1mb3VuZGF0aW9uLnZlcmNlbC5hcHAifX0seyJuYW1lIjoiZXhwbG9yZXItemVuIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NpYS1mb3VuZGF0aW9uL2V4cGxvcmVyLXplbi81NnJuVGdYMmFxNHk5WjFvYXVSaVpBQTdYNjREIiwicHJldmlld1VybCI6ImV4cGxvcmVyLXplbi1naXQtMDItMjYtdGVzdGV4cGxvcmVydXNlY2x1LTY4NjY3Yi1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImV4cGxvcmVyLXplbi1naXQtMDItMjYtdGVzdGV4cGxvcmVydXNlY2x1LTY4NjY3Yi1zaWEtZm91bmRhdGlvbi52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **explorer** | ✅ Ready ([Inspect](https://vercel.com/sia-foundation/explorer/Ez9U7T7gNtHMtcjatYvyt2YBkQbF)) | [Visit Preview](https://explorer-git-02-26-testexplorerusecluster-3766dd-sia-foundation.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/explorer-git-02-26-testexplorerusecluster-3766dd-sia-foundation.vercel.app?via=pr-comment-feedback-link) | Apr 4, 2025 7:51pm |
| **explorer-zen** | ✅ Ready ([Inspect](https://vercel.com/sia-foundation/explorer-zen/56rnTgX2aq4y9Z1oauRiZAA7X64D)) | [Visit Preview](https://explorer-zen-git-02-26-testexploreruseclu-68667b-sia-foundation.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/explorer-zen-git-02-26-testexploreruseclu-68667b-sia-foundation.vercel.app?via=pr-comment-feedback-link) | Apr 4, 2025 7:51pm |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **hostd** | ⬜️ Ignored ([Inspect](https://vercel.com/sia-foundation/hostd/8W7HEUaukWgJJ4kFQLbz6Zex2JyE)) | [Visit Preview](https://hostd-git-02-26-testexploreruseclusterfortesting-sia-foundation.vercel.app) |  | Apr 4, 2025 7:51pm |
| **renterd** | ⬜️ Ignored ([Inspect](https://vercel.com/sia-foundation/renterd/4G6J581TMzTWQmH8MzbFPAFMJTXi)) | [Visit Preview](https://renterd-git-02-26-testexploreruseclusterf-ea3f2a-sia-foundation.vercel.app) |  | Apr 4, 2025 7:51pm |
| **website** | ⬜️ Ignored ([Inspect](https://vercel.com/sia-foundation/website/Hk2Bj8gxvLBbGhyxK71Dr39qdxBQ)) | [Visit Preview](https://website-git-02-26-testexploreruseclusterf-49afc8-sia-foundation.vercel.app) |  | Apr 4, 2025 7:51pm |
</details>






author_association: NONE
user: login: telestrial
created_at: 2025-04-02T02:46:35Z
body: * **#1027** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1027?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1027?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#1018** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1018?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1017** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1017?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1026** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1026?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1032** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1032?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1034** <a href="https://app.graphite.dev/github/pr/SiaFoundation/web/1034?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: CONTRIBUTOR
user: login: telestrial
created_at: 2025-04-04T19:02:56Z
body: ### Merge activity

* **Apr 4, 3:02 PM EDT**: A user started a stack merge that includes this pull request via [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/1027).
* **Apr 4, 3:26 PM EDT**: [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/1027) couldn't merge this pull request because a downstack PR #1018 failed to merge.
* **Apr 4, 3:41 PM EDT**: A user started a stack merge that includes this pull request via [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/1027).
* **Apr 4, 3:43 PM EDT**: [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/1027) rebased this pull request as part of a merge.
* **Apr 4, 3:47 PM EDT**: A user merged this pull request with [Graphite](https://app.graphite.dev/github/pr/SiaFoundation/web/1027).

author_association: CONTRIBUTOR