feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
This commit is contained in:
@@ -1,87 +1,150 @@
|
||||
/**
|
||||
* Explorer Frontend E2E Tests
|
||||
* Tests live SPA links, route coverage, and detail-page navigation on explorer.d-bis.org.
|
||||
* Run: npx playwright test explorer-monorepo/scripts/e2e-explorer-frontend.spec.ts --project=chromium
|
||||
*/
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
const EXPLORER_URL = process.env.EXPLORER_URL || 'https://explorer.d-bis.org'
|
||||
const ADDRESS_TEST = '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506'
|
||||
|
||||
async function expectBodyToContain(page: Parameters<typeof test>[0], pattern: RegExp) {
|
||||
const bodyText = await page.locator('body').textContent().catch(() => '') || ''
|
||||
expect(bodyText).toMatch(pattern)
|
||||
async function expectHeading(page: Page, name: RegExp) {
|
||||
await expect(page.getByRole('heading', { name })).toBeVisible({ timeout: 10000 })
|
||||
}
|
||||
|
||||
test.describe('Explorer Frontend - Path Coverage', () => {
|
||||
function collectUnexpectedConsoleErrors(page: Page, allowlist: RegExp[] = []) {
|
||||
const unexpectedErrors: string[] = []
|
||||
|
||||
page.on('console', (message) => {
|
||||
if (message.type() !== 'error') {
|
||||
return
|
||||
}
|
||||
|
||||
const text = message.text()
|
||||
if (allowlist.some((pattern) => pattern.test(text))) {
|
||||
return
|
||||
}
|
||||
|
||||
unexpectedErrors.push(text)
|
||||
})
|
||||
|
||||
return async () => {
|
||||
expect(unexpectedErrors).toEqual([])
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Explorer Frontend - Route Coverage', () => {
|
||||
for (const route of [
|
||||
{ path: '/', matcher: /SolaceScanScout|Latest Blocks|Explorer/i },
|
||||
{ path: '/blocks', matcher: /Blocks|Block/i },
|
||||
{ path: '/transactions', matcher: /Transactions|Transaction/i },
|
||||
{ path: '/addresses', matcher: /Addresses|Address/i },
|
||||
{ path: '/watchlist', matcher: /Watchlist|Explorer/i },
|
||||
{ path: '/pools', matcher: /Pools|Liquidity/i },
|
||||
{ path: '/liquidity', matcher: /Liquidity|Route|Pool/i },
|
||||
{ path: '/bridge', matcher: /Bridge|Explorer/i },
|
||||
{ path: '/weth', matcher: /WETH|Explorer/i },
|
||||
{ path: '/', heading: /SolaceScanScout/i },
|
||||
{ path: '/blocks', heading: /^Blocks$/i },
|
||||
{ path: '/transactions', heading: /^Transactions$/i },
|
||||
{ path: '/addresses', heading: /^Addresses$/i },
|
||||
{ path: '/watchlist', heading: /^Watchlist$/i },
|
||||
{ path: '/pools', heading: /^Pools$/i },
|
||||
{ path: '/liquidity', heading: /Public liquidity, route discovery, and execution access points/i },
|
||||
{ path: '/wallet', heading: /Wallet & MetaMask/i },
|
||||
{ path: '/tokens', heading: /^Tokens$/i },
|
||||
{ path: '/search', heading: /^Search$/i },
|
||||
]) {
|
||||
test(`${route.path} loads`, async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}${route.path}`, { waitUntil: 'networkidle', timeout: 20000 })
|
||||
await page.goto(`${EXPLORER_URL}${route.path}`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expect(page).toHaveURL(new RegExp(route.path === '/' ? '/?$' : route.path.replace('/', '\\/')), { timeout: 8000 })
|
||||
await expectBodyToContain(page, route.matcher)
|
||||
await expectHeading(page, route.heading)
|
||||
})
|
||||
}
|
||||
|
||||
test('/address/:address loads address detail', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/address/${ADDRESS_TEST}`, { waitUntil: 'networkidle', timeout: 20000 })
|
||||
await page.waitForSelector('#addressDetailBreadcrumb', { state: 'attached', timeout: 15000 })
|
||||
await expectBodyToContain(page, /Address|Balance|Transaction|Explorer/i)
|
||||
test('/addresses/:address loads address detail', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/addresses/${ADDRESS_TEST}`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expectHeading(page, /Address/i)
|
||||
await expect(page.getByText(/Back to addresses/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Explorer Frontend - Nav and Detail Links', () => {
|
||||
test('MetaMask Snap link is present', async ({ page }) => {
|
||||
await page.goto(EXPLORER_URL, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
const snapLink = page.locator('a[href="/snap/"]').first()
|
||||
await expect(snapLink).toBeVisible({ timeout: 8000 })
|
||||
await expect(snapLink).toHaveAttribute('href', '/snap/')
|
||||
test.describe('Explorer Frontend - Current Navigation', () => {
|
||||
test('global shell is present on both app and pages routes', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/wallet`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expect(page.getByRole('link', { name: /Go to explorer home/i })).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/Support:/i)).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await page.goto(`${EXPLORER_URL}/transactions`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expect(page.getByRole('link', { name: /Go to explorer home/i })).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/Support:/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('Address breadcrumb home link returns to root', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/address/${ADDRESS_TEST}`, { waitUntil: 'networkidle', timeout: 20000 })
|
||||
await page.waitForSelector('#addressDetailView.active #addressDetailBreadcrumb', { state: 'visible', timeout: 15000 })
|
||||
const homeLink = page.locator('#addressDetailView.active #addressDetailBreadcrumb a[href="/"]').first()
|
||||
await expect(homeLink).toBeVisible({ timeout: 8000 })
|
||||
await homeLink.click()
|
||||
await expect(page).toHaveURL(new RegExp(`${EXPLORER_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/?$`), { timeout: 8000 })
|
||||
test('wallet page exposes the current Chain 138 Snap action', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/wallet`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expect(page.getByRole('button', { name: /Install Open Snap/i })).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByText(/Chain 138 Open Snap/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('Blocks list opens block detail view', async ({ page }) => {
|
||||
test('blocks list links to a current block detail route', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/blocks`, { waitUntil: 'networkidle', timeout: 20000 })
|
||||
const blockLink = page.locator('#blocksList tbody tr:first-child td:first-child a').first()
|
||||
await expect(blockLink).toBeVisible({ timeout: 8000 })
|
||||
const blockLink = page.locator('a[href^="/blocks/"]').first()
|
||||
await expect(blockLink).toBeVisible({ timeout: 10000 })
|
||||
await blockLink.click()
|
||||
await expect(page).toHaveURL(/\/block\/\d+/, { timeout: 8000 })
|
||||
await expect(page.locator('#blockDetailView.active #blockDetailBreadcrumb')).toBeVisible({ timeout: 8000 })
|
||||
await expect(page).toHaveURL(/\/blocks\/\d+$/, { timeout: 10000 })
|
||||
await expect(page.getByText(/Back to blocks/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('Transactions list opens transaction detail view', async ({ page }) => {
|
||||
test('transactions list links to a current transaction detail route', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/transactions`, { waitUntil: 'networkidle', timeout: 20000 })
|
||||
const transactionLink = page.locator('#transactionsList tbody tr:first-child td:first-child a').first()
|
||||
await expect(transactionLink).toBeVisible({ timeout: 8000 })
|
||||
await transactionLink.click()
|
||||
await expect(page).toHaveURL(/\/tx\/0x[a-f0-9]+/i, { timeout: 8000 })
|
||||
await expect(page.locator('#transactionDetailView.active #transactionDetailBreadcrumb')).toBeVisible({ timeout: 8000 })
|
||||
const transactionLinks = page.locator('a[href^="/transactions/"]')
|
||||
const transactionCount = await transactionLinks.count()
|
||||
|
||||
if (transactionCount === 0) {
|
||||
await expect(page.getByText(/Recent transactions are unavailable right now/i)).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByRole('button', { name: /^Next$/i })).toBeDisabled()
|
||||
return
|
||||
}
|
||||
|
||||
const href = await transactionLinks.first().getAttribute('href')
|
||||
expect(href).toMatch(/^\/transactions\/0x[a-f0-9]+$/i)
|
||||
await page.goto(`${EXPLORER_URL}${href}`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expect(page).toHaveURL(/\/transactions\/0x[a-f0-9]+$/i, { timeout: 10000 })
|
||||
await expect(page.getByText(/Back to transactions/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('Addresses list opens address detail view', async ({ page }) => {
|
||||
test('addresses page opens a current address detail route', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/addresses`, { waitUntil: 'networkidle', timeout: 20000 })
|
||||
const addressLink = page.locator('#addressesList tbody tr:first-child td:first-child a').first()
|
||||
await expect(addressLink).toBeVisible({ timeout: 8000 })
|
||||
await expect(page.locator('#addressesList tbody tr').first()).not.toContainText('N/A')
|
||||
await addressLink.click()
|
||||
await expect(page).toHaveURL(/\/address\/0x[a-f0-9]+/i, { timeout: 8000 })
|
||||
await expect(page.locator('#addressDetailView.active #addressDetailBreadcrumb')).toBeVisible({ timeout: 8000 })
|
||||
await page.getByPlaceholder('0x...').fill(ADDRESS_TEST)
|
||||
await page.getByRole('button', { name: /Open address/i }).click()
|
||||
await expect(page).toHaveURL(new RegExp(`/addresses/${ADDRESS_TEST}$`, 'i'), { timeout: 10000 })
|
||||
await expect(page.getByText(/Back to addresses/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('homepage keeps recent blocks visible when stats are temporarily unavailable', async ({ page }) => {
|
||||
const assertConsole = collectUnexpectedConsoleErrors(page, [
|
||||
/api\/v2\/stats/i,
|
||||
/503/i,
|
||||
/service unavailable/i,
|
||||
])
|
||||
|
||||
await page.route(`${EXPLORER_URL}/api/v2/stats`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 503,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'service_unavailable' }),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route(new RegExp(`${EXPLORER_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/api/v2/blocks\\?.*`), async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
hash: '0xabc',
|
||||
height: 4321,
|
||||
timestamp: '2026-04-05T00:00:00.000Z',
|
||||
miner: { hash: '0xdef' },
|
||||
transaction_count: 7,
|
||||
gas_used: 21000,
|
||||
gas_limit: 30000000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
await page.goto(`${EXPLORER_URL}/`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
await expect(page.getByText(/Live network stats are temporarily unavailable/i)).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByRole('link', { name: /Block #4321/i })).toBeVisible({ timeout: 10000 })
|
||||
await assertConsole()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user