The gate enforced only health. explorer, apps and branded each printed a
warning and fell through to an unconditional 'gate passed', so run 62877
went green with three red jobs. One rule now covers every guarded job:
green or the gate is red.
The apps matrix named itself App E2E (${{ matrix.app }}), which
stringifies the leg mapping to 'Object' — five identically named jobs,
none identifying which leg failed. Use matrix.app.name.
Explorer E2E was one of the hidden failures: it asserted SPC serves
stats, but no SPC chain exists and the explorer config disables the slug,
so api-explore-spc answers a plain-text 404. Four specs each carried
their own copy of the chain list; EXPLORERS in env.ts is now the one
list, and SPC is absent from it with the reason recorded.
httpGetJson fed error-page bodies to JSON.parse, reporting a complaint
about byte 4 of '404 page not found' instead of the endpoint and status.
It now reports both.
104 lines
4.6 KiB
TypeScript
104 lines
4.6 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { getUrls, httpStatus, httpGetJson, snap, expectBlocksSection } from './helpers'
|
|
import { EXPLORERS } from './env'
|
|
|
|
const urls = getUrls()
|
|
|
|
// ── Explorer API — every indexed chain ──────────────────────────────
|
|
test.describe('Explorer API — Blockscout Stats', () => {
|
|
for (const { name, api } of EXPLORERS) {
|
|
test(`${name} — stats endpoint returns blocks`, async ({ request }) => {
|
|
// The production explorer has multiple API replicas; a cold replica may
|
|
// briefly return an empty stats object while its cache warms. Poll the
|
|
// actual invariant instead of turning that transient into a red deploy,
|
|
// while still failing if no replica reports indexed blocks for 90s.
|
|
await expect.poll(async () => {
|
|
const data = await httpGetJson(request, `${api}/stats`, 30_000) as Record<string, unknown>
|
|
const blocks = Number(data.total_blocks || 0)
|
|
console.log(` ${name}: ${blocks} blocks, ${data.total_transactions || 0} txs, ${data.total_addresses || 0} addrs`)
|
|
return blocks
|
|
}, {
|
|
timeout: 90_000,
|
|
intervals: [5_000, 10_000, 15_000],
|
|
}).toBeGreaterThan(0)
|
|
})
|
|
}
|
|
|
|
for (const { name, api } of EXPLORERS) {
|
|
test(`${name} — search API accessible`, async ({ request }) => {
|
|
const status = await httpStatus(request, `${api}/search?q=0x0000000000000000000000000000000000000000`)
|
|
expect([200, 404]).toContain(status)
|
|
})
|
|
}
|
|
})
|
|
|
|
// ── Explorer Frontend — every indexed chain ─────────────────────────
|
|
test.describe('Explorer Frontend — Page Load', () => {
|
|
for (const { name, ui: url } of EXPLORERS) {
|
|
test(`${name} explorer loads (${url})`, async ({ page }) => {
|
|
const res = await page.goto(url, { timeout: 60_000, waitUntil: 'domcontentloaded' })
|
|
expect(res?.status()).toBe(200)
|
|
// SPA may not have title immediately; verify body has content
|
|
await page.waitForTimeout(2000)
|
|
const bodyLen = (await page.textContent('body'))?.length || 0
|
|
expect(bodyLen).toBeGreaterThan(0)
|
|
await snap(page, `explorer-${name.toLowerCase().replace(/\s+/g, '-')}`)
|
|
})
|
|
}
|
|
})
|
|
|
|
// ── Explorer UI — Core Functionality ────────────────────────────────
|
|
test.describe('Explorer UI — Lux C-Chain Navigation', () => {
|
|
test('homepage shows blocks table', async ({ page }) => {
|
|
await expectBlocksSection(page, urls.exploreLux)
|
|
await snap(page, 'explorer-lux-homepage')
|
|
})
|
|
|
|
test('blocks page loads', async ({ page }) => {
|
|
await page.goto(`${urls.exploreLux}/blocks`, { waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(3000)
|
|
await snap(page, 'explorer-lux-blocks')
|
|
const bodyLen = (await page.textContent('body'))?.length || 0
|
|
expect(bodyLen).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('transactions page loads', async ({ page }) => {
|
|
await page.goto(`${urls.exploreLux}/txs`, { waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(3000)
|
|
await snap(page, 'explorer-lux-txs')
|
|
})
|
|
|
|
test('address lookup works (zero address)', async ({ page }) => {
|
|
await page.goto(
|
|
`${urls.exploreLux}/address/0x0000000000000000000000000000000000000000`,
|
|
{ waitUntil: 'domcontentloaded' }
|
|
)
|
|
await page.waitForTimeout(3000)
|
|
await snap(page, 'explorer-lux-address')
|
|
// Should show address page, not error
|
|
const content = await page.textContent('body')
|
|
expect(content?.toLowerCase()).toContain('0x0000')
|
|
})
|
|
|
|
test('API docs page loads', async ({ page }) => {
|
|
await page.goto(`${urls.exploreLux}/api-docs`, { waitUntil: 'domcontentloaded' })
|
|
await page.waitForTimeout(2000)
|
|
await snap(page, 'explorer-lux-api-docs')
|
|
})
|
|
})
|
|
|
|
// ── Explorer Cross-Chain Verification ───────────────────────────────
|
|
test.describe('Explorer Cross-Chain — Consistent State', () => {
|
|
test('Lux and Zoo explorers show non-zero blocks', async ({ request }) => {
|
|
const [luxStats, zooStats] = await Promise.all([
|
|
httpGetJson(request, `${urls.apiExploreLux}/stats`) as Promise<Record<string, unknown>>,
|
|
httpGetJson(request, `${urls.apiExploreZoo}/stats`) as Promise<Record<string, unknown>>,
|
|
])
|
|
const luxBlocks = Number(luxStats.total_blocks || 0)
|
|
const zooBlocks = Number(zooStats.total_blocks || 0)
|
|
console.log(` Lux explorer: ${luxBlocks} blocks, Zoo explorer: ${zooBlocks} blocks`)
|
|
expect(luxBlocks).toBeGreaterThan(0)
|
|
expect(zooBlocks).toBeGreaterThan(0)
|
|
})
|
|
})
|