Skip to content

test(dashboard): playwright smoke harness + PR check#981

Merged
oskarszoon merged 15 commits into
bsv-blockchain:mainfrom
oskarszoon:fix/teranode-svelte-runes
May 29, 2026
Merged

test(dashboard): playwright smoke harness + PR check#981
oskarszoon merged 15 commits into
bsv-blockchain:mainfrom
oskarszoon:fix/teranode-svelte-runes

Conversation

@oskarszoon

Copy link
Copy Markdown
Contributor

Summary

Adds a Playwright smoke test per dashboard route plus a fork-safe GitHub Actions workflow that runs on every PR touching ui/dashboard/**. Lands the regression net before the Svelte 5 runes migration in a follow-up PR (closes #977).

Coverage

One test per route: navigate, assert visible selector ([data-testid="page-root"] added to pages that lacked a stable hook), assert zero console errors and zero Svelte deprecation messages during nav + 1s idle. All network calls (REST + WebSocket) mocked via page.route() / routeWebSocket() so the harness is hermetic — no real backend needed.

12 routes covered: /, /home, /network, /p2p, /peers, /forks, /ancestors, /viewer, /settings, /admin, /wstest, /login. The /api group is server-only (just +server.ts handlers, no navigable page) so it's excluded.

What this PR doesn't do

  • npm run check is intentionally not a CI step. The legacy-mode baseline carries ~33 svelte-check errors that pre-date this PR; the runes migration (follow-up) clears them and re-introduces the check as a gate.
  • No visual regression / screenshot diffs. Smoke proves "page mounted + zero errors", nothing more.
  • WebSocket payloads are not asserted. The WS mock accepts connections and swallows sends to prevent reconnect-loop console noise.

CI safety (fork-friendly)

  • pull_request trigger with paths: filter so the job only runs on UI changes.
  • contents: read permission only. No secrets, no Docker Hub login, no PR comment writes.
  • All third-party actions pinned by full commit SHA.

Verification

  • npm run test:integration --prefix ui/dashboard — 12 passed locally (two consecutive runs).
  • Sanity check: temporarily injected console.error in /home, confirmed smoke failed with the captured error; restored, confirmed green.

Follow-up

  • Svelte 5 runes migration (closes ui/dashboard: migrate svelte 5 legacy mode to runes mode #977) lands in a follow-up PR on top of this. That PR re-enables npm run check as a CI gate once the baseline is clean and fattens fixtures further (e.g. block-graph data) so smoke catches more data-shape regressions.

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

No issues found. This PR adds comprehensive Playwright smoke tests for all dashboard routes with proper CI integration.

Review Summary:

The implementation is solid:

  • ✅ Hermetic test design with complete API/WebSocket mocking
  • ✅ Covers all 12 navigable routes plus click-through scenarios for /viewer detail pages
  • ✅ CI workflow properly secured (fork-safe: pinned actions by SHA, --ignore-scripts, read-only permissions)
  • ✅ Test IDs consistently applied (data-test-id) across components
  • ✅ Console error detection includes Svelte deprecation warnings
  • ✅ Mock fixtures match expected API shapes

Previously Addressed:

  • ✅ WebSocket mock correctly uses routeWebSocket without explicit connect (Playwright 1.55 handles handshake automatically in mock-only mode)
  • ✅ Test ID placement consistent (testId prop for PageWithMenu components, direct data-test-id for non-wrapped pages)

No action required.

Comment thread ui/dashboard/tests/smoke/mocks/ws.ts
Comment thread ui/dashboard/src/routes/ancestors/+page.svelte Outdated
@github-actions

Copy link
Copy Markdown
Contributor

@oskarszoon Thanks for the clarification on the WebSocket mock (in reply to inline comment on ws.ts:8)! You're correct — Playwright's routeWebSocket automatically completes the handshake in mock-only mode without requiring connectToServer(). The empirical evidence (all smokes passing with no WS errors) confirms this is working as intended. Issue resolved.

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-981 (9c6e551)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.625µ 1.638µ ~ 1.000
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 71.15n 71.50n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 71.12n 71.48n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 71.17n 71.58n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.63n 36.23n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 56.10n 61.92n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 126.5n 187.0n ~ 0.100
MiningCandidate_Stringify_Short-4 219.8n 229.0n ~ 0.100
MiningCandidate_Stringify_Long-4 1.654µ 1.686µ ~ 0.200
MiningSolution_Stringify-4 849.2n 853.7n ~ 0.400
BlockInfo_MarshalJSON-4 1.750µ 1.750µ ~ 1.000
NewFromBytes-4 126.2n 124.2n ~ 0.200
AddTxBatchColumnar_Validation-4 2.518µ 2.542µ ~ 1.000
OffsetValidationLoop-4 545.9n 545.3n ~ 1.000
Mine_EasyDifficulty-4 61.27µ 61.05µ ~ 0.700
Mine_WithAddress-4 7.026µ 7.082µ ~ 0.400
DiskTxMap_SetIfNotExists-4 3.481µ 3.532µ ~ 0.700
DiskTxMap_SetIfNotExists_Parallel-4 3.324µ 3.441µ ~ 0.400
DiskTxMap_ExistenceOnly-4 325.3n 317.9n ~ 0.100
Queue-4 190.6n 190.5n ~ 1.000
AtomicPointer-4 4.584n 5.102n ~ 0.200
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 923.3µ 881.1µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 824.8µ 795.7µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 129.3µ 116.3µ ~ 0.200
ReorgOptimizations/AllMarkFalse/New/10K-4 62.14µ 62.88µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/10K-4 67.79µ 60.38µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/10K-4 11.68µ 11.47µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 5.599µ 4.845µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 2.475µ 1.630µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.182m 9.613m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.09m 10.24m ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.175m 1.181m ~ 1.000
ReorgOptimizations/AllMarkFalse/New/100K-4 688.3µ 684.6µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/100K-4 645.7µ 602.9µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 291.7µ 290.9µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 51.93µ 50.28µ ~ 0.700
ReorgOptimizations/NodeFlags/New/100K-4 18.19µ 17.80µ ~ 0.700
TxMapSetIfNotExists-4 52.95n 52.52n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 40.25n 40.41n ~ 0.400
ChannelSendReceive-4 605.0n 610.6n ~ 0.100
BlockAssembler_AddTx-4 0.02144n 0.02174n ~ 1.000
AddNode-4 9.576 9.418 ~ 1.000
AddNodeWithMap-4 9.727 9.898 ~ 0.700
DirectSubtreeAdd/4_per_subtree-4 57.47n 57.97n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 28.87n 29.10n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 27.98n 28.38n ~ 1.000
DirectSubtreeAdd/1024_per_subtree-4 26.46n 26.57n ~ 0.200
DirectSubtreeAdd/2048_per_subtree-4 26.08n 26.21n ~ 0.200
SubtreeProcessorAdd/4_per_subtree-4 294.0n 290.8n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 296.0n 292.5n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 288.7n 286.7n ~ 0.400
SubtreeProcessorAdd/1024_per_subtree-4 282.9n 280.2n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 285.4n 285.3n ~ 0.800
SubtreeProcessorRotate/4_per_subtree-4 284.1n 280.2n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 277.9n 281.3n ~ 0.700
SubtreeProcessorRotate/256_per_subtree-4 285.6n 277.9n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 289.4n 280.8n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.27n 55.33n ~ 1.000
SubtreeNodeAddOnly/64_per_subtree-4 36.11n 36.13n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 35.21n 35.12n ~ 0.100
SubtreeNodeAddOnly/1024_per_subtree-4 34.42n 34.50n ~ 0.400
SubtreeCreationOnly/4_per_subtree-4 112.3n 111.3n ~ 0.800
SubtreeCreationOnly/64_per_subtree-4 354.5n 353.6n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.233µ 1.235µ ~ 1.000
SubtreeCreationOnly/1024_per_subtree-4 3.818µ 3.760µ ~ 0.400
SubtreeCreationOnly/2048_per_subtree-4 6.897µ 6.763µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 291.0n 281.6n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 286.2n 283.9n ~ 0.400
ParallelGetAndSetIfNotExists/1k_nodes-4 2.034m 2.017m ~ 0.700
ParallelGetAndSetIfNotExists/10k_nodes-4 5.496m 5.337m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.609m 7.461m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 10.02m 10.22m ~ 0.400
SequentialGetAndSetIfNotExists/1k_nodes-4 1.795m 1.774m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 4.643m 4.596m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 13.50m 14.16m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 25.03m 26.62m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.136m 2.071m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.606m 8.596m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.89m 13.40m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.818m 1.843m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.383m 8.338m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 44.42m 44.10m ~ 1.000
CalcBlockWork-4 531.6n 533.9n ~ 0.400
CalculateWork-4 713.9n 717.7n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.344µ 1.352µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_100-4 14.61µ 13.99µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_1000-4 127.1µ 127.5µ ~ 0.100
CatchupWithHeaderCache-4 104.6m 104.7m ~ 0.700
_prepareTxsPerLevel-4 462.9m 459.0m ~ 0.700
_prepareTxsPerLevelOrdered-4 4.461m 5.696m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 490.9m 496.0m ~ 1.000
_prepareTxsPerLevel_Comparison/Optimized-4 3.906m 4.492m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.363m 1.420m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 328.9µ 335.1µ ~ 0.400
SubtreeSizes/10k_tx_64_per_subtree-4 77.25µ 79.32µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 19.47µ 19.81µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.797µ 9.919µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 4.805µ 4.926µ ~ 0.200
SubtreeSizes/10k_tx_2k_per_subtree-4 2.403µ 2.436µ ~ 0.200
BlockSizeScaling/10k_tx_64_per_subtree-4 76.19µ 77.15µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 19.11µ 19.39µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.794µ 4.872µ ~ 1.000
BlockSizeScaling/50k_tx_64_per_subtree-4 401.5µ 407.1µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 96.16µ 96.07µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.49µ 23.76µ ~ 0.400
SubtreeAllocations/small_subtrees_exists_check-4 161.4µ 163.7µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 164.9µ 168.3µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 332.9µ 333.2µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.668µ 9.585µ ~ 1.000
SubtreeAllocations/medium_subtrees_data_fetch-4 9.897µ 9.728µ ~ 0.400
SubtreeAllocations/medium_subtrees_full_validation-4 19.59µ 19.39µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.296µ 2.318µ ~ 0.300
SubtreeAllocations/large_subtrees_data_fetch-4 2.374µ 2.370µ ~ 1.000
SubtreeAllocations/large_subtrees_full_validation-4 4.869µ 4.834µ ~ 0.700
_BufferPoolAllocation/16KB-4 4.020µ 3.752µ ~ 0.100
_BufferPoolAllocation/32KB-4 8.992µ 11.613µ ~ 0.200
_BufferPoolAllocation/64KB-4 17.29µ 20.50µ ~ 0.700
_BufferPoolAllocation/128KB-4 33.69µ 37.28µ ~ 0.100
_BufferPoolAllocation/512KB-4 117.8µ 132.3µ ~ 0.100
_BufferPoolConcurrent/32KB-4 20.49µ 19.73µ ~ 0.700
_BufferPoolConcurrent/64KB-4 31.70µ 30.69µ ~ 0.100
_BufferPoolConcurrent/512KB-4 161.9µ 154.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 642.3µ 665.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 637.1µ 667.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 632.6µ 664.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 633.5µ 656.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 658.4µ 661.1µ ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.26m 37.42m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.73m 36.86m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.12m 37.44m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.47m 37.10m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.71m 36.81m ~ 0.700
_PooledVsNonPooled/Pooled-4 679.7n 836.3n ~ 0.100
_PooledVsNonPooled/NonPooled-4 8.383µ 8.464µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.992µ 7.978µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 12.39µ 11.57µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 10.18µ 12.16µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 250.6µ 254.1µ ~ 0.400
StoreBlock_Sequential/AboveCSVHeight-4 252.5µ 254.8µ ~ 1.000
GetUtxoHashes-4 256.9n 257.7n ~ 0.400
GetUtxoHashes_ManyOutputs-4 44.35µ 42.61µ ~ 0.100
_NewMetaDataFromBytes-4 230.5n 229.9n ~ 1.000
_Bytes-4 406.1n 404.2n ~ 0.400
_MetaBytes-4 138.7n 137.9n ~ 0.500

Threshold: >10% with p < 0.05 | Generated: 2026-05-29 08:54 UTC

@ordishs

ordishs commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Review

Nice harness — the fork-safe CI in particular is carefully done. One confirmed blocker before merge, plus some cleanup.

🔴 Blocker: the new wrapper <div>s break layout on 5 pages

Five routes get a new, unstyled wrapper (ancestors, network, peers, viewer, viewer/[type]) rather than an attribute on an existing element. They render inside ContentMenu's .page-content, which is:

.page-content { display: flex; flex-direction: column; align-items: center; width: 100%; }

With align-items: center (not stretch), a flex item only spans full width if it sets its own width — and the cards do (Card is .tui-card.stretch { width: 100% }, stretch defaults to true). Previously the card was the direct flex item, so width:100% resolved against the full container. After wrapping, the bare <div> (width auto) becomes a shrink-to-fit flex item, and the card's width:100% now resolves against the wrapper instead.

I reproduced the exact CSS chain and measured rendered widths in headless Chrome at 1440px:

Content Before (card is direct child) After (bare wrapper)
Sparse (e.g. peers returns [], like the smoke fixtures) 1031px — fills 105px — collapses to a centered sliver
Wide table (prod, 64-char hashes) 1031px — constrained, scrolls internally 1390px — overflows the 1213px container → horizontal scrollbar

So it breaks in both directions depending on content width. Critically, the smoke harness this PR adds cannot detect itpage-root is still "visible" and there are no console errors, so the tests stay green on a visibly broken page (which is also why "12 passed locally" didn't surface it).

Fix (verified): give each new wrapper width: 100%:

<div data-testid="page-root" style="width: 100%">

With that, sparse and wide both render at 1031px — identical to pre-PR. (display: contents is not an option: Playwright treats a box-less element as not visible, so toBeVisible() would fail.) The seven pages that reused an existing styled div (admin, home, forks, p2p, settings, login, wstest) are unaffected — no change needed there.

🟡 Convention

The codebase already uses data-test-id (hyphenated) 20×, and PageWithMenu exposes a testId prop that emits it onto a full-width .content-container. This PR introduces data-testid (no hyphen, 0 prior uses). Worth aligning the spelling — and for the seven PageWithMenu pages, <PageWithMenu testId="…"> would have provided the hook on an already-full-width element, sidestepping the wrapper issue entirely (login/wstest don't use PageWithMenu, so they'd still need their own).

🟡 Test quality

  • src/routes/+page.svelte just renders <Home />, so the / test and the standalone smoke: /home describe exercise the same component, and /home duplicates the ROUTES loop body — fold it into ROUTES.
  • The click-through's final assertion reuses [data-testid="page-root"], which also exists on the list page it navigated from — weak proof the detail page rendered, plus a strict-mode multi-match risk if both roots are briefly mounted during transition. Prefer a detail-unique selector (e.g. the block-hash heading).

⚪ Nits

  • Allowlist has a dead entry (Download the React DevTools — that's React, not Svelte) and an over-broad one (bare echarts substring would also swallow a genuine ECharts error).
  • waitForTimeout(1000) ×13 is a hard sleep (~13s of pure wait); fine for a late-console window but inherently soft.
  • Playwright browser cache has no restore-keys, so any lockfile change forces a full re-download.
  • The npm ci --ignore-scripts path is only exercised in CI, not in the local test:integration verification — it'll first be proven on this PR's own run.

What's solid

  • Fork-safe CI: pull_request (not pull_request_target), permissions: contents: read, all actions SHA-pinned, --ignore-scripts, direct ./node_modules/.bin/playwright invocation. Model example.
  • Catch-all-first mock ordering correctly matches Playwright's LIFO resolution.
  • The blocks regex correctly excludes blockstats via the (?:\?|$) anchor.
  • Lockfile is consistent — base already resolves @playwright/test@1.57.0, which satisfies the new ^1.55.0 floor, so npm ci is fine.
  • Checked one concern and cleared it: vitest won't try to run the Playwright .spec.ts files — vite.config.ts scopes test.include to src/**, and the specs live in tests/.

Verdict

One confirmed blocker — the wrapper divs regress layout on 5 production pages, undetectable by the harness being added. It's a two-line-per-file fix (width: 100% on each new wrapper), verified to restore original rendering. Everything else is convention/cleanup. Fix the wrappers and this is a strong addition.

…test-id

Addresses review on bsv-blockchain#981:
- Drop the new wrapper <div>s on 5 pages that regressed layout (the bare
  wrappers became shrink-to-fit flex items, collapsing or overflowing the
  cards they wrapped). PageWithMenu already exposes a testId prop that
  emits data-test-id onto its full-width .content-container — use that.
- Migrate all 10 PageWithMenu consumers to the testId prop. login/wstest
  (no PageWithMenu) keep their existing styled element; rename to the
  repo's data-test-id convention.
- Fold /home into the ROUTES loop (it shared a component with /).
- Click-through assertion now waits for the block hash text on the detail
  page — a detail-page-unique signal instead of the reused page-root.
- Trim console allowlist: drop "Download the React DevTools" (not Svelte)
  and the bare "echarts" substring (too broad).
- Add restore-keys to the Playwright browser cache so unrelated lockfile
  bumps don't force a full re-download.
@oskarszoon

Copy link
Copy Markdown
Contributor Author

Thanks for the layout catch — that was a real bug the harness couldn't see itself. Pushed 259f010bd which addresses everything:

Blocker — wrapper layout regression: Migrated all 10 PageWithMenu consumers to use the existing testId="page-root" prop (emits data-test-id on the full-width .content-container). All 5 new wrappers gone. login/wstest (no PageWithMenu) keep their existing styled element. No new layout-affecting wrappers anywhere.

Convention: All data-testiddata-test-id, matching the existing 20× usage.

Test quality:

  • Folded /home into the ROUTES loop (it just renders <Home />).
  • Click-through now asserts the full 64-char block hash text on the detail page — a detail-card-unique signal, dodges the multi-match risk on transition.

Nits:

  • Dropped Download the React DevTools and the bare echarts substring from the allowlist.
  • Added restore-keys: pw-${{ runner.os }}- to the Playwright browser cache.
  • waitForTimeout(1000) × 12 kept for now — pragmatic late-console window. Open to swapping for networkidle in a follow-up if it shows in CI runtime.
  • --ignore-scripts path: yes, first proven on this PR's own CI run. Accepted.

…llowlist

Addresses review findings on bsv-blockchain#981:
- Add subtree/tx/utxo detail smokes (direct nav with fixtures). The
  click-through only covered /viewer/block; the other three /viewer/[type]
  components use the same Svelte 4 patterns the runes migration targets, so
  the net now guards all four detail types.
- Add subtree/tx/txmeta mock routes + fixtures (disjoint regexes; raw bodies
  the api client wraps in {ok, data}).
- Fix block-detail.json merkleroot: was 66 hex chars, now 64.
- Scope the auth-401 console allowlist to `authenticated: false` tests only,
  so a genuine 401 on an authenticated route still fails the smoke.
@oskarszoon

Copy link
Copy Markdown
Contributor Author

Follow-up review surfaced a few more items beyond @ordishs's. Pushed 8a9ab22ff:

Coverage — /viewer/[type] gap. The click-through only exercised block; subtree/tx/utxo use the same Svelte 4 patterns the runes migration targets and weren't smoked. Added direct-navigation smokes (with fixtures) for all three, so the net guards all four detail types — 16 tests total now.

Fixture bug. block-detail.json merkleroot was 66 hex chars; fixed to 64.

Allowlist scope. The auth-401 console-log entry was global, which would swallow a genuine 401 on an authenticated route. Scoped it to authenticated: false tests only.

Documented limitation. data-test-id="page-root" is the page shell, so the route smokes prove "mounted + no console error" but not visible layout/content correctness — the exact class @ordishs caught manually. The 4 viewer detail tests add content-bearing checks; broader per-route content assertions are deferred to the runes-migration PR where the components churn anyway.

Pre-existing dashboard issues found during review (dup best_height key, peers DOM-index traversal, admin FSM logic dup) are out of scope here — tracked in #982.

Still deferring waitForTimeoutnetworkidle to a follow-up.

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified locally on the PR head (8a9ab22ff): the full Playwright smoke suite is green — 16/16 passed across two consecutive runs (retries disabled, exit 0). Covers all 12 navigable routes, the /viewer block-detail click-through, and the subtree/tx/utxo detail pages.

Strong points:

  • Fork-safe CIpull_request (not pull_request_target), contents: read only, all third-party actions SHA-pinned, npm ci --ignore-scripts, and the locally-resolved Playwright binary. No secrets exposed.
  • Hermetic harness — REST + WebSocket fully mocked, so no backend dependency and no reconnect-loop console noise.
  • Mock route precedence (LIFO, catch-all first) and the dual prod/dev URL patterns are correct.

Non-blocking follow-ups (fine to fold into the runes PR):

  • .npmrc sets engine-strict=true, so the new engines: node >=20.19.0 is now a hard gate on npm install in ui/dashboard (including make build-dashboard). Defensible given Vite 7, but worth pinning the CI Node minor or adding an .nvmrc so the workflow can't drift below the floor the package now requires.
  • Smoke can't distinguish an auth redirect: /login also carries data-test-id="page-root" and the SPA fallback makes every response 200, so an authed route that silently redirected would still pass the visibility check. Consider asserting the final URL or a route-unique selector.

Approving.

@sonarqubecloud

Copy link
Copy Markdown

@oskarszoon oskarszoon merged commit 48cb339 into bsv-blockchain:main May 29, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ui/dashboard: migrate svelte 5 legacy mode to runes mode

4 participants