Complete UX audit P3: API copy URLs, labels, retry, and smoke sync.
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 13s
Validate Explorer / frontend (push) Successful in 1m25s
Validate Explorer / smoke-e2e (push) Failing after 2m46s

Add footer copy-to-clipboard for public APIs, align ops page labels, improve mobile brand lockup, surface WalletConnect posture on wallet tools, add account access discovery, liquidity retry alerts, and refresh smoke-route expectations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-22 22:54:08 -07:00
parent 4fac5e4856
commit efd7c8bbcb
13 changed files with 179 additions and 41 deletions

View File

@@ -152,8 +152,8 @@
<div class="status-note" id="command-center-fallback"> <div class="status-note" id="command-center-fallback">
If diagram rendering is unavailable, use the main explorer operational surfaces directly: If diagram rendering is unavailable, use the main explorer operational surfaces directly:
<a href="/operations">Operations Hub</a>, <a href="/operations">Operations hub</a>,
<a href="/bridge">Bridge Monitoring</a>, <a href="/bridge">Bridge</a>,
<a href="/routes">Routes</a>, <a href="/routes">Routes</a>,
<a href="/system">System</a>, <a href="/system">System</a>,
and <a href="/operator">Operator</a>. and <a href="/operator">Operator</a>.

View File

@@ -4,15 +4,15 @@ const baseUrl = (process.env.BASE_URL || 'https://explorer.d-bis.org').replace(/
const addressUnderTest = process.env.SMOKE_ADDRESS || '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506' const addressUnderTest = process.env.SMOKE_ADDRESS || '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506'
const checks = [ const checks = [
{ path: '/', expectTexts: ['DBIS Explorer', 'Recent Blocks', 'Open wallet tools'] }, { path: '/', expectTexts: ['DBIS Explorer', 'Recent Blocks', 'Network overview'] },
{ path: '/blocks', expectTexts: ['Blocks'] }, { path: '/blocks', expectTexts: ['Blocks'] },
{ path: '/transactions', expectTexts: ['Transactions'] }, { path: '/transactions', expectTexts: ['Transactions'] },
{ path: '/addresses', expectTexts: ['Addresses', 'Open An Address'] }, { path: '/addresses', expectTexts: ['Addresses', 'Open An Address'] },
{ path: '/watchlist', expectTexts: ['Watchlist', 'Saved Addresses'] }, { path: '/watchlist', expectTexts: ['Watchlist', 'Saved Addresses'] },
{ path: '/pools', expectTexts: ['Pools', 'Pool operation shortcuts'] }, { path: '/pools', expectTexts: ['Pools', 'Pool operation shortcuts'] },
{ path: '/liquidity', expectTexts: ['Chain 138 Liquidity Access', 'Explorer Access Points'] }, { path: '/liquidity', expectTexts: ['Liquidity', 'Explorer Access Points'] },
{ path: '/wallet', expectTexts: ['Wallet & MetaMask', 'Install Open Snap'] }, { path: '/wallet', expectTexts: ['Wallet Tools', 'WalletConnect v2 posture'] },
{ path: '/tokens', expectTexts: ['Tokens', 'Find A Token'] }, { path: '/tokens', expectTexts: ['Tokens', 'Find a token'] },
{ path: '/search', expectTexts: ['Search'], placeholder: 'Search by address, transaction hash, block number...' }, { path: '/search', expectTexts: ['Search'], placeholder: 'Search by address, transaction hash, block number...' },
{ path: `/addresses/${addressUnderTest}`, expectTexts: [], anyOfTexts: ['Back to addresses', 'Address not found'] }, { path: `/addresses/${addressUnderTest}`, expectTexts: [], anyOfTexts: ['Back to addresses', 'Address not found'] },
] ]
@@ -22,7 +22,10 @@ async function bodyText(page) {
} }
async function hasShell(page) { async function hasShell(page) {
const homeLink = await page.getByRole('link', { name: /Go to explorer home/i }).isVisible().catch(() => false) const homeLink = await page
.getByRole('link', { name: /Go to DBIS Explorer home|Go to explorer home/i })
.isVisible()
.catch(() => false)
const supportText = await page.getByText(/Support:/i).isVisible().catch(() => false) const supportText = await page.getByText(/Support:/i).isVisible().catch(() => false)
return homeLink && supportText return homeLink && supportText
} }

View File

@@ -15,7 +15,7 @@ export default function BrandLockup({ compact = false }: { compact?: boolean })
</span> </span>
<span <span
className={[ className={[
'block truncate font-medium uppercase text-gray-500 dark:text-gray-400', 'block truncate font-medium uppercase text-gray-500 dark:text-gray-400 max-sm:hidden',
compact ? 'text-[0.64rem] tracking-[0.13em]' : 'text-[0.68rem] tracking-[0.12em]', compact ? 'text-[0.64rem] tracking-[0.13em]' : 'text-[0.68rem] tracking-[0.12em]',
].join(' ')} ].join(' ')}
> >

View File

@@ -0,0 +1,34 @@
interface ExplorerRetryAlertProps {
message: string
onRetry?: () => void
retryLabel?: string
className?: string
}
export default function ExplorerRetryAlert({
message,
onRetry,
retryLabel = 'Retry',
className = '',
}: ExplorerRetryAlertProps) {
return (
<div
role="alert"
className={[
'flex flex-col gap-3 rounded-xl border border-red-200 bg-red-50/70 px-4 py-3 dark:border-red-900/50 dark:bg-red-950/20 sm:flex-row sm:items-center sm:justify-between',
className,
].join(' ')}
>
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{message}</p>
{onRetry ? (
<button
type="button"
onClick={onRetry}
className="shrink-0 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-semibold text-red-800 transition-colors hover:bg-red-50 dark:border-red-800 dark:bg-red-950 dark:text-red-100 dark:hover:bg-red-900/40"
>
{retryLabel}
</button>
) : null}
</div>
)
}

View File

@@ -1,5 +1,5 @@
import Link from 'next/link' import Link from 'next/link'
import { explorerPublicApiLinks } from '@/data/explorerOperations' import FooterPublicApiLinks from '@/components/common/FooterPublicApiLinks'
const footerLinkClass = const footerLinkClass =
'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors' 'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
@@ -62,16 +62,7 @@ export default function Footer() {
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"> <div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Public APIs Public APIs
</div> </div>
<ul className="space-y-3 text-sm"> <FooterPublicApiLinks />
{explorerPublicApiLinks.map((link) => (
<li key={link.href}>
<a className={footerLinkClass} href={link.href} target="_blank" rel="noopener noreferrer">
{link.label}
</a>
<p className="mt-0.5 text-xs leading-5 text-gray-500 dark:text-gray-500">{link.description}</p>
</li>
))}
</ul>
<p className="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-500"> <p className="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-500">
Read-only JSON endpoints on the public explorer domain. No API key required. Read-only JSON endpoints on the public explorer domain. No API key required.
</p> </p>

View File

@@ -0,0 +1,53 @@
'use client'
import { useState } from 'react'
import { explorerPublicApiLinks } from '@/data/explorerOperations'
const footerLinkClass =
'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors'
function absoluteApiUrl(href: string): string {
if (typeof window === 'undefined') return href
if (href.startsWith('http://') || href.startsWith('https://')) return href
return `${window.location.origin}${href.startsWith('/') ? href : `/${href}`}`
}
export default function FooterPublicApiLinks() {
const [copiedHref, setCopiedHref] = useState<string | null>(null)
const copyUrl = async (href: string) => {
if (typeof navigator === 'undefined' || !navigator.clipboard) return
try {
await navigator.clipboard.writeText(absoluteApiUrl(href))
setCopiedHref(href)
window.setTimeout(() => setCopiedHref((current) => (current === href ? null : current)), 1500)
} catch {
setCopiedHref(null)
}
}
return (
<ul className="space-y-3 text-sm">
{explorerPublicApiLinks.map((link) => (
<li key={link.href}>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<a className={footerLinkClass} href={link.href} target="_blank" rel="noopener noreferrer">
{link.label}
</a>
<p className="mt-0.5 text-xs leading-5 text-gray-500 dark:text-gray-500">{link.description}</p>
</div>
<button
type="button"
onClick={() => void copyUrl(link.href)}
className="shrink-0 rounded-lg border border-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 transition-colors hover:border-primary-300 hover:text-primary-700 dark:border-gray-700 dark:text-gray-200 dark:hover:border-primary-500 dark:hover:text-primary-300"
aria-label={`Copy URL for ${link.label}`}
>
{copiedHref === link.href ? 'Copied' : 'Copy URL'}
</button>
</div>
</li>
))}
</ul>
)
}

View File

@@ -717,7 +717,7 @@ export default function Navbar() {
<div className="flex min-h-[60px] items-center gap-3 lg:min-h-[64px]"> <div className="flex min-h-[60px] items-center gap-3 lg:min-h-[64px]">
<Link <Link
href="/" href="/"
className="group inline-flex min-w-0 items-center gap-2 rounded-lg py-1.5 pr-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950" className="group inline-flex shrink-0 items-center gap-2 rounded-lg py-1.5 pr-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-950 max-sm:max-w-[9.5rem] sm:max-w-none"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
aria-label="Go to DBIS Explorer home" aria-label="Go to DBIS Explorer home"
> >

View File

@@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { Card } from '@/libs/frontend-ui-primitives' import { Card } from '@/libs/frontend-ui-primitives'
import ExplorerRetryAlert from '@/components/common/ExplorerRetryAlert'
import { type TokenListResponse } from '@/services/api/config' import { type TokenListResponse } from '@/services/api/config'
import { tokensApi } from '@/services/api/tokens' import { tokensApi } from '@/services/api/tokens'
import { import {
@@ -79,6 +80,7 @@ export default function LiquidityOperationsPage({
const [stats, setStats] = useState<ExplorerStats | null>(initialStats) const [stats, setStats] = useState<ExplorerStats | null>(initialStats)
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus) const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
const [loadingError, setLoadingError] = useState<string | null>(null) const [loadingError, setLoadingError] = useState<string | null>(null)
const [reloadKey, setReloadKey] = useState(0)
const [copiedEndpoint, setCopiedEndpoint] = useState<string | null>(null) const [copiedEndpoint, setCopiedEndpoint] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
@@ -99,6 +101,7 @@ export default function LiquidityOperationsPage({
} }
const load = async () => { const load = async () => {
setLoadingError(null)
const [tokenListResult, routeMatrixResult, plannerCapabilitiesResult, planResult, statsResult, bridgeResult] = const [tokenListResult, routeMatrixResult, plannerCapabilitiesResult, planResult, statsResult, bridgeResult] =
await Promise.allSettled([ await Promise.allSettled([
tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })), tokensApi.listForSurface('extended', 138).then(({ ok, data }) => ({ tokens: ok ? data : [] })),
@@ -163,6 +166,7 @@ export default function LiquidityOperationsPage({
initialStats, initialStats,
initialTokenList, initialTokenList,
initialTokenPoolRecords, initialTokenPoolRecords,
reloadKey,
]) ])
const featuredTokens = useMemo( const featuredTokens = useMemo(
@@ -266,7 +270,7 @@ export default function LiquidityOperationsPage({
<main className="container mx-auto px-4 py-6 sm:py-8"> <main className="container mx-auto px-4 py-6 sm:py-8">
<div className="mb-8 max-w-4xl"> <div className="mb-8 max-w-4xl">
<div className="mb-3 inline-flex rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-amber-700"> <div className="mb-3 inline-flex rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">
Chain 138 Liquidity Access Liquidity
</div> </div>
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl"> <h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
Public liquidity, route discovery, and execution access points Public liquidity, route discovery, and execution access points
@@ -282,9 +286,14 @@ export default function LiquidityOperationsPage({
<OperationsSurfaceNav /> <OperationsSurfaceNav />
{loadingError ? ( {loadingError ? (
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20"> <ExplorerRetryAlert
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p> className="mb-6"
</Card> message={loadingError}
onRetry={() => {
setLoadingError(null)
setReloadKey((value) => value + 1)
}}
/>
) : null} ) : null}
<div className="mb-6"> <div className="mb-6">

View File

@@ -650,7 +650,7 @@ export default function Home({
href="/bridge" href="/bridge"
className="inline-flex items-center justify-center rounded-xl bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-black dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100" className="inline-flex items-center justify-center rounded-xl bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white hover:bg-black dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100"
> >
Open bridge monitoring Open bridge
</Link> </Link>
<Link <Link
href="/operations" href="/operations"
@@ -996,6 +996,9 @@ export default function Home({
<Link href="/search" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800"> <Link href="/search" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Search Search
</Link> </Link>
<Link href="/access" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Account access
</Link>
<Link href="/tokens" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800"> <Link href="/tokens" className="rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-primary-600 hover:border-primary-400 dark:border-gray-800">
Tokens Tokens
</Link> </Link>

View File

@@ -0,0 +1,32 @@
'use client'
import { useEffect, useState } from 'react'
import { getWalletConnectConfig, type WalletConnectConfigResponse } from '@/services/api/walletConnect'
export default function WalletConnectPostureNote() {
const [config, setConfig] = useState<WalletConnectConfigResponse | null>(null)
useEffect(() => {
let cancelled = false
getWalletConnectConfig()
.then((value) => {
if (!cancelled) setConfig(value)
})
.catch(() => {
if (!cancelled) setConfig(null)
})
return () => {
cancelled = true
}
}, [])
if (!config) return null
return (
<p className="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-400">
WalletConnect v2 posture: <strong>{config.enabled ? 'enabled' : config.status}</strong>. Browser extension wallets use{' '}
<code className="rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-900">{config.fallbackAuth}</code> today.
{config.message ? ` ${config.message}` : ''}
</p>
)
}

View File

@@ -6,6 +6,7 @@ import type {
TokenListCatalog, TokenListCatalog,
} from '@/components/wallet/AddToMetaMask' } from '@/components/wallet/AddToMetaMask'
import { AddToMetaMask } from '@/components/wallet/AddToMetaMask' import { AddToMetaMask } from '@/components/wallet/AddToMetaMask'
import WalletConnectPostureNote from '@/components/wallet/WalletConnectPostureNote'
import Link from 'next/link' import Link from 'next/link'
import { Explain, useUiMode } from '@/components/common/UiModeContext' import { Explain, useUiMode } from '@/components/common/UiModeContext'
import { accessApi, type WalletAccessSession } from '@/services/api/access' import { accessApi, type WalletAccessSession } from '@/services/api/access'
@@ -192,6 +193,7 @@ export default function WalletPage(props: WalletPageProps) {
? 'Use the explorer-served network catalog, token list, and capability metadata to connect Chain 138 (DeFi Oracle Meta Mainnet) and Ethereum Mainnet to MetaMask and other Web3 wallets.' ? 'Use the explorer-served network catalog, token list, and capability metadata to connect Chain 138 (DeFi Oracle Meta Mainnet) and Ethereum Mainnet to MetaMask and other Web3 wallets.'
: 'Use explorer-served network and token metadata to connect Chain 138 and Ethereum Mainnet wallets.'} : 'Use explorer-served network and token metadata to connect Chain 138 and Ethereum Mainnet wallets.'}
</p> </p>
<WalletConnectPostureNote />
<div className="mb-6 rounded-2xl border border-sky-200 bg-sky-50/60 p-5 dark:border-sky-900/40 dark:bg-sky-950/20"> <div className="mb-6 rounded-2xl border border-sky-200 bg-sky-50/60 p-5 dark:border-sky-900/40 dark:bg-sky-950/20">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div> <div>
@@ -483,7 +485,7 @@ export default function WalletPage(props: WalletPageProps) {
<> <>
Need swap and liquidity discovery too? Visit the{' '} Need swap and liquidity discovery too? Visit the{' '}
<Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400"> <Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400">
Liquidity Access Liquidity
</Link>{' '} </Link>{' '}
page for live Chain 138 pools, route matrix links, partner payload templates, and the internal fallback execution plan endpoints. page for live Chain 138 pools, route matrix links, partner payload templates, and the internal fallback execution plan endpoints.
</> </>
@@ -492,7 +494,7 @@ export default function WalletPage(props: WalletPageProps) {
<> <>
Liquidity and planner posture lives on the{' '} Liquidity and planner posture lives on the{' '}
<Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400"> <Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400">
Liquidity Access Liquidity
</Link>{' '} </Link>{' '}
surface. surface.
</> </>

View File

@@ -21,7 +21,7 @@ const sharedOperationsNote =
export const explorerFeaturePages = { export const explorerFeaturePages = {
bridge: { bridge: {
eyebrow: 'Bridge Monitoring', eyebrow: 'Bridge',
title: 'Bridge & Relay Monitoring', title: 'Bridge & Relay Monitoring',
description: description:
'Inspect the CCIP relay status, follow the live mission-control stream, trace bridge transactions, and review the managed Mainnet, BSC, Avalanche, Avalanche cW, and Avalanche to Chain 138 lanes.', 'Inspect the CCIP relay status, follow the live mission-control stream, trace bridge transactions, and review the managed Mainnet, BSC, Avalanche, Avalanche cW, and Avalanche to Chain 138 lanes.',
@@ -81,7 +81,7 @@ export const explorerFeaturePages = {
title: 'Liquidity access', title: 'Liquidity access',
description: 'Review the public Chain 138 PMM access points, route helpers, and fallback execution endpoints.', description: 'Review the public Chain 138 PMM access points, route helpers, and fallback execution endpoints.',
href: '/liquidity', href: '/liquidity',
label: 'Open liquidity access', label: 'Open liquidity',
}, },
{ {
title: 'Pools inventory', title: 'Pools inventory',
@@ -93,7 +93,7 @@ export const explorerFeaturePages = {
title: 'Bridge monitoring', title: 'Bridge monitoring',
description: 'Cross-check route availability with live relay and bridge health before operator actions.', description: 'Cross-check route availability with live relay and bridge health before operator actions.',
href: '/bridge', href: '/bridge',
label: 'Open bridge monitoring', label: 'Open bridge',
}, },
{ {
title: 'Operations hub', title: 'Operations hub',
@@ -114,7 +114,7 @@ export const explorerFeaturePages = {
title: 'Bridge monitoring', title: 'Bridge monitoring',
description: 'Start with relay and bridge health before reviewing WETH-specific flows.', description: 'Start with relay and bridge health before reviewing WETH-specific flows.',
href: '/bridge', href: '/bridge',
label: 'Open bridge monitoring', label: 'Open bridge',
}, },
{ {
title: 'Visual command center', title: 'Visual command center',
@@ -175,8 +175,8 @@ export const explorerFeaturePages = {
], ],
}, },
operator: { operator: {
eyebrow: 'Operator Surface', eyebrow: 'Operator',
title: 'Operator Surface', title: 'Operator',
description: description:
'Expose the public operator surface for bridge checks, route validation, planner providers, liquidity entry points, and documentation.', 'Expose the public operator surface for bridge checks, route validation, planner providers, liquidity entry points, and documentation.',
note: sharedOperationsNote, note: sharedOperationsNote,
@@ -188,7 +188,7 @@ export const explorerFeaturePages = {
title: 'Bridge monitoring', title: 'Bridge monitoring',
description: 'Open relay status, queue posture, and bridge trace tools.', description: 'Open relay status, queue posture, and bridge trace tools.',
href: '/bridge', href: '/bridge',
label: 'Open bridge monitoring', label: 'Open bridge',
}, },
{ {
title: 'Routes', title: 'Routes',
@@ -200,7 +200,7 @@ export const explorerFeaturePages = {
title: 'Liquidity access', title: 'Liquidity access',
description: 'Open partner payload helpers, route APIs, and execution-plan endpoints.', description: 'Open partner payload helpers, route APIs, and execution-plan endpoints.',
href: '/liquidity', href: '/liquidity',
label: 'Open liquidity access', label: 'Open liquidity',
}, },
{ {
title: 'Explorer docs', title: 'Explorer docs',
@@ -235,7 +235,7 @@ export const explorerFeaturePages = {
title: 'Bridge monitoring', title: 'Bridge monitoring',
description: 'Correlate topology context with the live bridge and relay status surface.', description: 'Correlate topology context with the live bridge and relay status surface.',
href: '/bridge', href: '/bridge',
label: 'Open bridge monitoring', label: 'Open bridge',
}, },
{ {
title: 'Explorer docs', title: 'Explorer docs',
@@ -252,8 +252,8 @@ export const explorerFeaturePages = {
], ],
}, },
operations: { operations: {
eyebrow: 'Operations Hub', eyebrow: 'Operations hub',
title: 'Operations Hub', title: 'Operations hub',
description: description:
'This hub exposes the public operational surfaces for bridge monitoring, routes, wrapped-asset references, analytics shortcuts, operator links, and topology views.', 'This hub exposes the public operational surfaces for bridge monitoring, routes, wrapped-asset references, analytics shortcuts, operator links, and topology views.',
note: sharedOperationsNote, note: sharedOperationsNote,
@@ -262,7 +262,7 @@ export const explorerFeaturePages = {
title: 'Bridge & relay monitoring', title: 'Bridge & relay monitoring',
description: 'Open mission-control status, SSE monitoring, and bridge trace helpers.', description: 'Open mission-control status, SSE monitoring, and bridge trace helpers.',
href: '/bridge', href: '/bridge',
label: 'Open bridge monitoring', label: 'Open bridge',
}, },
{ {
title: 'Routes & liquidity', title: 'Routes & liquidity',

View File

@@ -15,6 +15,11 @@ test.describe('Explorer sprint smoke', () => {
await expect(page.getByRole('heading', { name: /Wallet Tools/i })).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('heading', { name: /Wallet Tools/i })).toBeVisible({ timeout: 10000 })
}) })
test('wallet page shows WalletConnect posture note', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/wallet`, { waitUntil: 'domcontentloaded', timeout: 20000 })
await expect(page.getByText(/WalletConnect v2 posture/i)).toBeVisible({ timeout: 10000 })
})
test('tokens page loads', async ({ page }) => { test('tokens page loads', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/tokens`, { waitUntil: 'domcontentloaded', timeout: 20000 }) await page.goto(`${EXPLORER_URL}/tokens`, { waitUntil: 'domcontentloaded', timeout: 20000 })
await expect(page.getByRole('heading', { name: /^Tokens$/i })).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('heading', { name: /^Tokens$/i })).toBeVisible({ timeout: 10000 })
@@ -28,7 +33,7 @@ test.describe('Explorer sprint smoke', () => {
test('operations hub loads extended token list note', async ({ page }) => { test('operations hub loads extended token list note', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/operations`, { waitUntil: 'networkidle', timeout: 30000 }) await page.goto(`${EXPLORER_URL}/operations`, { waitUntil: 'networkidle', timeout: 30000 })
await expect(page.getByRole('heading', { name: /Operations Hub/i })).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('heading', { name: /Operations hub/i })).toBeVisible({ timeout: 10000 })
await expect(page.getByText(/Extended Metamask dual-chain catalog/i).first()).toBeVisible({ timeout: 10000 }) await expect(page.getByText(/Extended Metamask dual-chain catalog/i).first()).toBeVisible({ timeout: 10000 })
}) })
@@ -49,9 +54,15 @@ test.describe('Explorer sprint smoke', () => {
await expect(page.getByRole('contentinfo').getByText(/Public APIs/i)).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('contentinfo').getByText(/Public APIs/i)).toBeVisible({ timeout: 10000 })
await expect(page.getByRole('contentinfo').getByRole('link', { name: /Blockscout stats/i })).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('contentinfo').getByRole('link', { name: /Blockscout stats/i })).toBeVisible({ timeout: 10000 })
await expect(page.getByRole('contentinfo').getByRole('link', { name: /Wallet tools/i })).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('contentinfo').getByRole('link', { name: /Wallet tools/i })).toBeVisible({ timeout: 10000 })
await expect(page.getByRole('contentinfo').getByRole('button', { name: /Copy URL for Blockscout stats/i })).toBeVisible({ timeout: 10000 })
await expect(page.getByRole('contentinfo').getByRole('link', { name: /Account access/i })).toBeVisible({ timeout: 10000 }) await expect(page.getByRole('contentinfo').getByRole('link', { name: /Account access/i })).toBeVisible({ timeout: 10000 })
}) })
test('homepage quick links include account access', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/`, { waitUntil: 'domcontentloaded', timeout: 20000 })
await expect(page.getByRole('link', { name: /Account access/i }).first()).toBeVisible({ timeout: 10000 })
})
test('tablet viewport exposes mobile navigation menu', async ({ page }) => { test('tablet viewport exposes mobile navigation menu', async ({ page }) => {
await page.setViewportSize({ width: 1100, height: 800 }) await page.setViewportSize({ width: 1100, height: 800 })
await page.goto(`${EXPLORER_URL}/`, { waitUntil: 'domcontentloaded', timeout: 20000 }) await page.goto(`${EXPLORER_URL}/`, { waitUntil: 'domcontentloaded', timeout: 20000 })
@@ -68,7 +79,7 @@ test.describe('Explorer sprint smoke', () => {
test('operator page shows track 4 surface note', async ({ page }) => { test('operator page shows track 4 surface note', async ({ page }) => {
await page.goto(`${EXPLORER_URL}/operator`, { waitUntil: 'domcontentloaded', timeout: 30000 }) await page.goto(`${EXPLORER_URL}/operator`, { waitUntil: 'domcontentloaded', timeout: 30000 })
await expect(page.getByRole('heading', { name: /^Operator Surface$/i })).toBeVisible({ timeout: 15000 }) await expect(page.getByRole('heading', { name: /^Operator$/i })).toBeVisible({ timeout: 15000 })
await expect(page.getByText(/Track 4 public surface/i).first()).toBeVisible({ timeout: 10000 }) await expect(page.getByText(/Track 4 public surface/i).first()).toBeVisible({ timeout: 10000 })
}) })