From 26e90a01b67945e80666d48884a687853718dbdb Mon Sep 17 00:00:00 2001
From: LockedTerminal
Date: Sat, 29 Aug 2026 15:17:29 +0000
Subject: [PATCH] test: add zero-signer edge case test to ThresholdConfig
quorum preview
Closes #235
- Update ThresholdConfig.tsx to render an explicit 'No signers
registered' warning placeholder when signers list is empty and a
valid threshold is entered. This prevents the quorum preview area
from silently disappearing, which admins may interpret as a loading
or broken state rather than a meaningful empty state.
- Add ThresholdConfig.test.tsx with 4 tests:
- placeholder warning shown when signers list is empty
- no NaN / Infinity / undefined in the rendered output with zero signers
- quorum preview renders normally when signers are present
- infeasible-threshold warning shown when threshold exceeds total weight
---
.../ThresholdConfig/ThresholdConfig.test.tsx | 146 ++++++++++++++++++
.../ThresholdConfig/ThresholdConfig.tsx | 11 ++
2 files changed, 157 insertions(+)
create mode 100644 frontend/src/components/ThresholdConfig/ThresholdConfig.test.tsx
diff --git a/frontend/src/components/ThresholdConfig/ThresholdConfig.test.tsx b/frontend/src/components/ThresholdConfig/ThresholdConfig.test.tsx
new file mode 100644
index 0000000..8f07b3b
--- /dev/null
+++ b/frontend/src/components/ThresholdConfig/ThresholdConfig.test.tsx
@@ -0,0 +1,146 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import '@testing-library/jest-dom'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import ThresholdConfig from './ThresholdConfig'
+
+// ---------------------------------------------------------------------------
+// Fetch mock helpers
+// ---------------------------------------------------------------------------
+
+type FetchRoute = {
+ url: RegExp | string
+ response: unknown
+ status?: number
+}
+
+function setupFetch(routes: FetchRoute[]) {
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn((url: string) => {
+ const route = routes.find(r =>
+ typeof r.url === 'string' ? url.includes(r.url) : r.url.test(url),
+ )
+ const status = route?.status ?? 200
+ const body = route ? JSON.stringify(route.response) : '{}'
+ return Promise.resolve({
+ ok: status >= 200 && status < 300,
+ status,
+ json: () => Promise.resolve(route?.response ?? {}),
+ text: () => Promise.resolve(body),
+ })
+ }),
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe('ThresholdConfig — zero-signer edge case (issue #235)', () => {
+ afterEach(() => {
+ vi.unstubAllGlobals()
+ vi.restoreAllMocks()
+ })
+
+ it('shows a placeholder warning instead of a broken/empty quorum preview when signers list is empty', async () => {
+ setupFetch([
+ { url: /threshold/, response: { threshold: 2 } },
+ { url: /signers/, response: [] }, // ← zero signers
+ ])
+
+ render()
+
+ // Wait for the component to finish loading
+ await waitFor(() => {
+ expect(screen.queryByText(/Loading current value/i)).not.toBeInTheDocument()
+ })
+
+ // Enter a valid threshold value so quorum preview section becomes visible
+ const input = screen.getByLabelText(/new threshold/i)
+ fireEvent.change(input, { target: { value: '3' } })
+
+ // The quorum preview section should appear with an informative message
+ await waitFor(() => {
+ expect(screen.getByText(/No signers registered/i)).toBeInTheDocument()
+ })
+ })
+
+ it('does NOT show a NaN or divide-by-zero value when signers list is empty', async () => {
+ setupFetch([
+ { url: /threshold/, response: { threshold: 5 } },
+ { url: /signers/, response: [] },
+ ])
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.queryByText(/Loading current value/i)).not.toBeInTheDocument()
+ })
+
+ const input = screen.getByLabelText(/new threshold/i)
+ fireEvent.change(input, { target: { value: '5' } })
+
+ // Allow any async state settling
+ await waitFor(() => {
+ const bodyText = document.body.textContent ?? ''
+ expect(bodyText).not.toMatch(/NaN/)
+ expect(bodyText).not.toMatch(/Infinity/)
+ expect(bodyText).not.toMatch(/undefined/)
+ })
+ })
+
+ it('shows quorum preview normally when signers are present', async () => {
+ setupFetch([
+ { url: /threshold/, response: { threshold: 2 } },
+ {
+ url: /signers/,
+ response: [
+ { address: 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN', weight: 2 },
+ { address: 'GBVVJJPJZ3GR4VZVKNL4EOXTMQBQUXOUMGJXHWLZAGNNPPLZEXFQBVF', weight: 1 },
+ ],
+ },
+ ])
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.queryByText(/Loading current value/i)).not.toBeInTheDocument()
+ })
+
+ const input = screen.getByLabelText(/new threshold/i)
+ fireEvent.change(input, { target: { value: '2' } })
+
+ await waitFor(() => {
+ expect(screen.getByText(/Quorum Preview/i)).toBeInTheDocument()
+ expect(screen.getByText(/Required signers:/i)).toBeInTheDocument()
+ })
+
+ // No "no signers" warning when signers are actually present
+ expect(screen.queryByText(/No signers registered/i)).not.toBeInTheDocument()
+ })
+
+ it('shows infeasible threshold warning when threshold exceeds total signer weight', async () => {
+ setupFetch([
+ { url: /threshold/, response: { threshold: 1 } },
+ {
+ url: /signers/,
+ response: [
+ { address: 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN', weight: 1 },
+ ],
+ },
+ ])
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.queryByText(/Loading current value/i)).not.toBeInTheDocument()
+ })
+
+ const input = screen.getByLabelText(/new threshold/i)
+ fireEvent.change(input, { target: { value: '999' } })
+
+ await waitFor(() => {
+ expect(screen.getByText(/unreachable/i)).toBeInTheDocument()
+ })
+ })
+})
diff --git a/frontend/src/components/ThresholdConfig/ThresholdConfig.tsx b/frontend/src/components/ThresholdConfig/ThresholdConfig.tsx
index a34c2d8..b987686 100644
--- a/frontend/src/components/ThresholdConfig/ThresholdConfig.tsx
+++ b/frontend/src/components/ThresholdConfig/ThresholdConfig.tsx
@@ -167,6 +167,17 @@ export default function ThresholdConfig() {
)}
+ {/* Zero-signer edge case: show a clear placeholder instead of silently
+ rendering nothing, which could look like a loading or broken state. */}
+ {isValid && !signersLoading && signers.length === 0 && (
+
+
Quorum Preview
+
+ ⚠️ No signers registered. Add signers before configuring a threshold.
+
+
+ )}
+
{quorumPreview && (
Quorum Preview