Add mobile ops surface nav and footer public API links.
Operations pages get collapsible surface navigation on small screens and a shared action-card accordion; the footer surfaces read-only JSON endpoints with e2e coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
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'
|
||||
@@ -9,7 +10,7 @@ export default function Footer() {
|
||||
return (
|
||||
<footer className="mt-auto border-t border-gray-200 dark:border-gray-700 bg-white/90 dark:bg-gray-900/90 backdrop-blur">
|
||||
<div className="container mx-auto px-4 py-6 sm:py-8">
|
||||
<div className="grid gap-4 sm:gap-6 md:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<div className="grid gap-4 sm:gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-3 rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white sm:text-lg">
|
||||
DBIS Explorer
|
||||
@@ -46,6 +47,26 @@ export default function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
|
||||
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Public APIs
|
||||
</div>
|
||||
<ul className="space-y-3 text-sm">
|
||||
{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">
|
||||
Read-only JSON endpoints on the public explorer domain. No API key required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-gray-800 dark:bg-gray-900/40 md:border-0 md:bg-transparent md:p-0">
|
||||
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Contact
|
||||
|
||||
@@ -19,6 +19,8 @@ import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import { bridgeRoutesApi, normalizeBridgeRouteEntries, type BridgeRoutesResponse } from '@/services/api/bridgeRoutes'
|
||||
import { createVisibilityAwarePoller } from '@/utils/visibilityRefresh'
|
||||
import { HOME_DASHBOARD_REFRESH_MS } from '@/utils/featuredTokens'
|
||||
import OperationsSurfaceNav from './OperationsSurfaceNav'
|
||||
import OperationsActionGrid from './OperationsActionGrid'
|
||||
|
||||
type FeedState = 'connecting' | 'live' | 'fallback'
|
||||
|
||||
@@ -327,6 +329,8 @@ export default function BridgeMonitoringPage({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<OperationsSurfaceNav />
|
||||
|
||||
<div className="mb-6">
|
||||
<ActivityContextPanel context={activityContext} title="Bridge Freshness Context" />
|
||||
<FreshnessTrustNote
|
||||
@@ -493,27 +497,7 @@ export default function BridgeMonitoringPage({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{page.actions.map((action) => (
|
||||
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{action.title}
|
||||
</div>
|
||||
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{action.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<ActionLink
|
||||
href={action.href}
|
||||
label={action.label}
|
||||
external={'external' in action ? action.external : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<OperationsActionGrid actions={page.actions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import MarketEvidenceNote from '@/components/common/MarketEvidenceNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
import OperationsSurfaceNav from './OperationsSurfaceNav'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import {
|
||||
formatCurrency,
|
||||
@@ -278,6 +279,8 @@ export default function LiquidityOperationsPage({
|
||||
<TokenListSurfaceNote className="mt-3 text-sm text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
|
||||
<OperationsSurfaceNav />
|
||||
|
||||
{loadingError ? (
|
||||
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
||||
|
||||
73
frontend/src/components/explorer/OperationsActionGrid.tsx
Normal file
73
frontend/src/components/explorer/OperationsActionGrid.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import type { ExplorerFeatureAction } from '@/data/explorerOperations'
|
||||
|
||||
export function OperationsActionLink({ action }: { action: ExplorerFeatureAction }) {
|
||||
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
|
||||
const label = `${action.label} ->`
|
||||
|
||||
if (action.external) {
|
||||
return (
|
||||
<a href={action.href} className={className} target="_blank" rel="noopener noreferrer">
|
||||
{label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={action.href} className={className}>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionCard({ action }: { action: ExplorerFeatureAction }) {
|
||||
return (
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">{action.title}</div>
|
||||
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">{action.description}</p>
|
||||
<div className="mt-4">
|
||||
<OperationsActionLink action={action} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function OperationsActionGrid({
|
||||
actions,
|
||||
title = 'Quick actions',
|
||||
}: {
|
||||
actions: ExplorerFeatureAction[]
|
||||
title?: string
|
||||
}) {
|
||||
if (actions.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<details className="group mb-6 rounded-2xl border border-gray-200 bg-gray-50/80 dark:border-gray-800 dark:bg-gray-900/40 md:hidden">
|
||||
<summary className="cursor-pointer list-none px-4 py-3 text-sm font-semibold text-gray-900 dark:text-white [&::-webkit-details-marker]:hidden">
|
||||
<span className="flex items-center justify-between gap-3">
|
||||
{title}
|
||||
<span className="text-xs font-normal uppercase tracking-wide text-gray-500">
|
||||
{actions.length} links · <span className="group-open:hidden">Show</span>
|
||||
<span className="hidden group-open:inline">Hide</span>
|
||||
</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div className="space-y-3 border-t border-gray-200 px-3 py-3 dark:border-gray-800">
|
||||
{actions.map((action) => (
|
||||
<ActionCard key={`${action.title}-${action.href}`} action={action} />
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="hidden gap-4 md:grid lg:grid-cols-2">
|
||||
{actions.map((action) => (
|
||||
<ActionCard key={`${action.title}-${action.href}`} action={action} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
import OperationsSurfaceNav from '@/components/explorer/OperationsSurfaceNav'
|
||||
import OperationsActionGrid from '@/components/explorer/OperationsActionGrid'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import { statsApi, type ExplorerStats } from '@/services/api/stats'
|
||||
|
||||
@@ -192,6 +194,8 @@ export default function OperationsHubPage({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<OperationsSurfaceNav />
|
||||
|
||||
{loadingError ? (
|
||||
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
||||
@@ -338,27 +342,7 @@ export default function OperationsHubPage({
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{page.actions.map((action) => (
|
||||
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{action.title}
|
||||
</div>
|
||||
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{action.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<ActionLink
|
||||
href={action.href}
|
||||
label={action.label}
|
||||
external={'external' in action ? action.external : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<OperationsActionGrid actions={page.actions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import type { ExplorerFeatureAction, ExplorerFeaturePage } from '@/data/explorerOperations'
|
||||
import type { ExplorerFeaturePage } from '@/data/explorerOperations'
|
||||
import OperationsSurfaceNav from './OperationsSurfaceNav'
|
||||
import OperationsActionGrid from './OperationsActionGrid'
|
||||
|
||||
export type StatusTone = 'normal' | 'warning' | 'danger'
|
||||
|
||||
function ActionLink({ action }: { action: ExplorerFeatureAction }) {
|
||||
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
|
||||
const label = `${action.label} ->`
|
||||
|
||||
if (action.external) {
|
||||
return (
|
||||
<a href={action.href} className={className} target="_blank" rel="noopener noreferrer">
|
||||
{label}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={action.href} className={className}>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function relativeAge(isoString?: string): string {
|
||||
if (!isoString) return 'Unknown'
|
||||
const parsed = Date.parse(isoString)
|
||||
@@ -126,23 +108,11 @@ export default function OperationsPageShell({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<OperationsSurfaceNav />
|
||||
|
||||
{children}
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{page.actions.map((action) => (
|
||||
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">{action.title}</div>
|
||||
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{action.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<ActionLink action={action} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<OperationsActionGrid actions={page.actions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
79
frontend/src/components/explorer/OperationsSurfaceNav.tsx
Normal file
79
frontend/src/components/explorer/OperationsSurfaceNav.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import clsx from 'clsx'
|
||||
import { explorerOperationsSurfaces } from '@/data/explorerOperations'
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
if (path.length > 1 && path.endsWith('/')) {
|
||||
return path.slice(0, -1)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export default function OperationsSurfaceNav({ className }: { className?: string }) {
|
||||
const router = useRouter()
|
||||
const currentPath = normalizePath(router.pathname)
|
||||
|
||||
return (
|
||||
<nav aria-label="Operations surfaces" className={clsx('mb-6', className)}>
|
||||
<details className="group rounded-2xl border border-gray-200 bg-gray-50/80 dark:border-gray-800 dark:bg-gray-900/40 md:hidden">
|
||||
<summary className="cursor-pointer list-none px-4 py-3 text-sm font-semibold text-gray-900 dark:text-white [&::-webkit-details-marker]:hidden">
|
||||
<span className="flex items-center justify-between gap-3">
|
||||
Jump to operations surface
|
||||
<span className="text-xs font-normal uppercase tracking-wide text-gray-500 group-open:hidden">Show</span>
|
||||
<span className="hidden text-xs font-normal uppercase tracking-wide text-gray-500 group-open:inline">Hide</span>
|
||||
</span>
|
||||
</summary>
|
||||
<ul className="space-y-1 border-t border-gray-200 px-2 py-2 dark:border-gray-800">
|
||||
{explorerOperationsSurfaces.map((surface) => {
|
||||
const active = currentPath === surface.href
|
||||
return (
|
||||
<li key={surface.href}>
|
||||
<Link
|
||||
href={surface.href}
|
||||
className={clsx(
|
||||
'block rounded-xl px-3 py-2 transition',
|
||||
active
|
||||
? 'bg-primary-50 text-primary-700 dark:bg-primary-950/40 dark:text-primary-200'
|
||||
: 'text-gray-700 hover:bg-white dark:text-gray-300 dark:hover:bg-gray-950/60',
|
||||
)}
|
||||
>
|
||||
<div className="text-sm font-semibold">{surface.label}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{surface.description}</div>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">
|
||||
Operations surfaces
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{explorerOperationsSurfaces.map((surface) => {
|
||||
const active = currentPath === surface.href
|
||||
return (
|
||||
<Link
|
||||
key={surface.href}
|
||||
href={surface.href}
|
||||
title={surface.description}
|
||||
className={clsx(
|
||||
'rounded-full border px-3 py-1.5 text-sm font-medium transition',
|
||||
active
|
||||
? 'border-primary-500 bg-primary-50 text-primary-700 dark:border-primary-400 dark:bg-primary-950/40 dark:text-primary-200'
|
||||
: 'border-gray-200 text-gray-700 hover:border-primary-300 hover:text-primary-600 dark:border-gray-700 dark:text-gray-300 dark:hover:border-primary-500 dark:hover:text-primary-300',
|
||||
)}
|
||||
>
|
||||
{surface.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { type TokenListResponse } from '@/services/api/config'
|
||||
import { tokensApi } from '@/services/api/tokens'
|
||||
import TokenListSurfaceNote from '@/components/common/TokenListSurfaceNote'
|
||||
import OperationsSurfaceNav from './OperationsSurfaceNav'
|
||||
import {
|
||||
aggregateLiquidityPools,
|
||||
getRouteBackedPoolAddresses,
|
||||
@@ -105,6 +106,8 @@ export default function PoolsOperationsPage() {
|
||||
<TokenListSurfaceNote className="mt-3 text-sm text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
|
||||
<OperationsSurfaceNav />
|
||||
|
||||
{loadingError ? (
|
||||
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
||||
|
||||
@@ -16,6 +16,8 @@ import ActivityContextPanel from '@/components/common/ActivityContextPanel'
|
||||
import FreshnessTrustNote from '@/components/common/FreshnessTrustNote'
|
||||
import SubsystemPosturePanel from '@/components/common/SubsystemPosturePanel'
|
||||
import { resolveEffectiveFreshness } from '@/utils/explorerFreshness'
|
||||
import OperationsSurfaceNav from './OperationsSurfaceNav'
|
||||
import OperationsActionGrid from './OperationsActionGrid'
|
||||
|
||||
interface RoutesMonitoringPageProps {
|
||||
initialRouteMatrix?: RouteMatrixResponse | null
|
||||
@@ -224,6 +226,8 @@ export default function RoutesMonitoringPage({
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<OperationsSurfaceNav />
|
||||
|
||||
{loadingError ? (
|
||||
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
||||
@@ -438,27 +442,7 @@ export default function RoutesMonitoringPage({
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{page.actions.map((action) => (
|
||||
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{action.title}
|
||||
</div>
|
||||
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{action.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<ActionLink
|
||||
href={action.href}
|
||||
label={action.label}
|
||||
external={Boolean((action as { external?: boolean }).external)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<OperationsActionGrid actions={page.actions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,3 +296,65 @@ export const explorerFeaturePages = {
|
||||
],
|
||||
},
|
||||
} as const satisfies Record<string, ExplorerFeaturePage>
|
||||
|
||||
export interface ExplorerOperationsSurface {
|
||||
href: string
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const explorerOperationsSurfaces: ExplorerOperationsSurface[] = [
|
||||
{
|
||||
href: '/operations',
|
||||
label: 'Operations hub',
|
||||
description: 'Consolidated monitoring, config, and route inventory.',
|
||||
},
|
||||
{
|
||||
href: '/bridge',
|
||||
label: 'Bridge',
|
||||
description: 'Relay lanes, mission-control feed, and CCIP routes.',
|
||||
},
|
||||
{
|
||||
href: '/routes',
|
||||
label: 'Routes',
|
||||
description: 'Live route matrix and execution paths.',
|
||||
},
|
||||
{
|
||||
href: '/liquidity',
|
||||
label: 'Liquidity',
|
||||
description: 'PMM access points and planner capabilities.',
|
||||
},
|
||||
{
|
||||
href: '/pools',
|
||||
label: 'Pools',
|
||||
description: 'Mission-control pool inventory snapshot.',
|
||||
},
|
||||
{
|
||||
href: '/system',
|
||||
label: 'System',
|
||||
description: 'Networks, RPC methods, and topology inventory.',
|
||||
},
|
||||
]
|
||||
|
||||
export const explorerPublicApiLinks = [
|
||||
{
|
||||
href: '/api/v2/stats',
|
||||
label: 'Blockscout stats',
|
||||
description: 'Chain head, gas, and indexer summary.',
|
||||
},
|
||||
{
|
||||
href: '/explorer-api/v1/track1/bridge/status',
|
||||
label: 'Bridge status JSON',
|
||||
description: 'Mission-control relay posture snapshot.',
|
||||
},
|
||||
{
|
||||
href: '/token-aggregation/api/v1/routes/matrix?includeNonLive=true',
|
||||
label: 'Route matrix',
|
||||
description: 'Token-aggregation live and planned routes.',
|
||||
},
|
||||
{
|
||||
href: '/api/config/networks',
|
||||
label: 'Wallet networks',
|
||||
description: 'Published chain metadata for wallet onboarding.',
|
||||
},
|
||||
] as const
|
||||
|
||||
@@ -37,4 +37,17 @@ test.describe('Explorer sprint smoke', () => {
|
||||
await expect(page.getByRole('heading', { name: /Bridge & Relay Monitoring/i })).toBeVisible({ timeout: 15000 })
|
||||
await expect(page.getByText(/CCIP route catalog/i).first()).toBeVisible({ timeout: 15000 })
|
||||
})
|
||||
|
||||
test('operations hub shows surface navigation', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 })
|
||||
await page.goto(`${EXPLORER_URL}/operations`, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
||||
await expect(page.getByRole('navigation', { name: /Operations surfaces/i })).toBeVisible({ timeout: 10000 })
|
||||
await expect(page.getByRole('link', { name: /Bridge/i }).first()).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('footer lists public API endpoints', async ({ page }) => {
|
||||
await page.goto(`${EXPLORER_URL}/`, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
||||
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 })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user