From 459182a0b74cfd9c1c5e950c8efa13018b0085ba Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Tue, 28 Apr 2026 20:50:29 +0100 Subject: [PATCH 01/22] Add error state tracking to TelemetryDashboard --- .../src/components/TelemetryDashboard.jsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index d06ae380..5a318939 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -37,11 +37,13 @@ export default function TelemetryDashboard({ addToast }) { const { demoEnabled } = useDemoMode(); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const [exporting, setExporting] = useState(false); const [syncing, setSyncing] = useState(false); const loadData = useCallback(() => { setLoading(true); + setError(null); if (demoEnabled) { setTimeout(() => { @@ -81,7 +83,10 @@ export default function TelemetryDashboard({ addToast }) { try { const data = analytics.getSummary(); setSummary(data); + setError(null); } catch (err) { + const errorMessage = err.message || 'Failed to load telemetry data'; + setError(errorMessage); console.error('Failed to load telemetry:', err); } finally { setLoading(false); @@ -170,6 +175,51 @@ export default function TelemetryDashboard({ addToast }) { ); } + if (error) { + return ( +
+
+
+ +
+

+ Failed to Load Telemetry Data +

+

+ {error} +

+
+ + + +
+
+
+
+
+ ); + } + const vitalsSummary = computeVitalsSummary(summary?.webVitals || {}); const tipFunnel = computeTipFunnel(summary || {}); const batchFunnel = computeBatchFunnel(summary || {}); From cc4e6abc7a416750d26422a5e768de3e3b583354 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Tue, 28 Apr 2026 20:52:49 +0100 Subject: [PATCH 02/22] Add comprehensive error state tests for TelemetryDashboard --- .../test/TelemetryDashboard.error.test.jsx | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 frontend/src/test/TelemetryDashboard.error.test.jsx diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx new file mode 100644 index 00000000..72394535 --- /dev/null +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -0,0 +1,292 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import TelemetryDashboard from '../components/TelemetryDashboard'; + +const mockAnalytics = { + getSummary: vi.fn(), + reset: vi.fn(), +}; + +vi.mock('../lib/analytics', () => ({ + analytics: mockAnalytics, +})); + +vi.mock('../context/DemoContext', () => ({ + useDemoMode: vi.fn(() => ({ + demoEnabled: false, + setDemoBalance: vi.fn(), + })), +})); + +vi.mock('../lib/telemetry-env', () => ({ + getEnvironmentLabel: vi.fn(() => 'PRODUCTION'), + getEnvironmentColor: vi.fn(() => 'green'), +})); + +vi.mock('../lib/telemetry-vitals', () => ({ + computeVitalsSummary: vi.fn(() => ({ + vitals: [], + overallScore: null, + coreVitalsPassing: false, + })), +})); + +vi.mock('../lib/telemetry-funnel', () => ({ + computeTipFunnel: vi.fn(() => ({ + stages: [], + overallConversion: 0, + cancelled: 0, + failed: 0, + })), + computeBatchFunnel: vi.fn(() => ({ + stages: [], + overallConversion: 0, + cancelled: 0, + failed: 0, + })), + computeWalletDropOff: vi.fn(() => ({ + connections: 0, + disconnections: 0, + retentionRate: 0, + dropOffRate: 0, + })), + identifyDropOffPoints: vi.fn(() => []), +})); + +vi.mock('../lib/telemetry-export', () => ({ + downloadExport: vi.fn(() => 'export.json'), + exportToCsv: vi.fn(() => 'export.csv'), + copyToClipboard: vi.fn(), +})); + +vi.mock('../lib/telemetry-sink', () => ({ + isSinkEnabled: vi.fn(() => false), + getSinkConfig: vi.fn(() => ({ endpoint: '' })), + sendSnapshot: vi.fn(), +})); + +vi.mock('../lib/telemetry-storage', () => ({ + getStorageUsage: vi.fn(() => ({ telemetryBytes: 0 })), +})); + +describe('TelemetryDashboard error handling', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('displays error state when analytics.getSummary throws', async () => { + const errorMessage = 'Analytics service unavailable'; + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error(errorMessage); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + }); + + it('shows retry button in error state', async () => { + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + render(); + + await waitFor(() => { + const retryButton = screen.getByRole('button', { name: /retry/i }); + expect(retryButton).toBeInTheDocument(); + }); + }); + + it('keeps export buttons available during error state', async () => { + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + render(); + + await waitFor(() => { + const exportJsonButton = screen.getByRole('button', { name: /export json/i }); + const exportCsvButton = screen.getByRole('button', { name: /export csv/i }); + expect(exportJsonButton).toBeInTheDocument(); + expect(exportCsvButton).toBeInTheDocument(); + }); + }); + + it('allows retry after error', async () => { + const user = userEvent.setup(); + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + const { rerender } = render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + mockAnalytics.getSummary.mockReturnValue({ + sessions: 10, + totalPageViews: 50, + tipsConfirmed: 5, + tipCompletionRate: '50', + tipDropOffRate: '50', + batchTipsConfirmed: 1, + batchCompletionRate: '100', + totalErrors: 0, + sortedPages: [], + sortedErrors: [], + webVitals: {}, + walletConnections: 5, + walletDisconnections: 0, + firstSeen: Date.now(), + lastSeen: Date.now(), + }); + + const retryButton = screen.getByRole('button', { name: /retry/i }); + await user.click(retryButton); + + await waitFor(() => { + expect(screen.queryByText('Failed to Load Telemetry Data')).not.toBeInTheDocument(); + }); + }); + + it('clears error when data loads successfully', async () => { + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + const { rerender } = render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + mockAnalytics.getSummary.mockReturnValue({ + sessions: 10, + totalPageViews: 50, + tipsConfirmed: 5, + tipCompletionRate: '50', + tipDropOffRate: '50', + batchTipsConfirmed: 1, + batchCompletionRate: '100', + totalErrors: 0, + sortedPages: [], + sortedErrors: [], + webVitals: {}, + walletConnections: 5, + walletDisconnections: 0, + firstSeen: Date.now(), + lastSeen: Date.now(), + }); + + rerender(); + + await waitFor(() => { + expect(screen.queryByText('Failed to Load Telemetry Data')).not.toBeInTheDocument(); + }); + }); + + it('displays generic error message for unknown errors', async () => { + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error(); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + }); + + it('shows error with specific message from exception', async () => { + const specificError = 'Database connection timeout'; + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error(specificError); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText(specificError)).toBeInTheDocument(); + }); + }); + + it('maintains error state across re-renders', async () => { + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Persistent error'); + }); + + const { rerender } = render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + rerender(); + + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + it('exports JSON even when data load fails', async () => { + const user = userEvent.setup(); + const mockDownloadExport = vi.fn(() => 'export.json'); + + vi.doMock('../lib/telemetry-export', () => ({ + downloadExport: mockDownloadExport, + exportToCsv: vi.fn(() => 'export.csv'), + copyToClipboard: vi.fn(), + })); + + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + const addToast = vi.fn(); + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + const exportJsonButton = screen.getByRole('button', { name: /export json/i }); + await user.click(exportJsonButton); + + await waitFor(() => { + expect(addToast).toHaveBeenCalledWith(expect.stringContaining('Exported'), 'success'); + }); + }); + + it('exports CSV even when data load fails', async () => { + const user = userEvent.setup(); + const mockExportCsv = vi.fn(() => 'export.csv'); + + vi.doMock('../lib/telemetry-export', () => ({ + downloadExport: vi.fn(() => 'export.json'), + exportToCsv: mockExportCsv, + copyToClipboard: vi.fn(), + })); + + mockAnalytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + const addToast = vi.fn(); + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + const exportCsvButton = screen.getByRole('button', { name: /export csv/i }); + await user.click(exportCsvButton); + + await waitFor(() => { + expect(addToast).toHaveBeenCalledWith(expect.stringContaining('Exported'), 'success'); + }); + }); +}); From 546bf7c5435b3c825cbbfaaa01afcc429620e2d5 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Tue, 28 Apr 2026 20:55:25 +0100 Subject: [PATCH 03/22] Fix error state test to use retry button instead of rerender --- .../test/TelemetryDashboard.error.test.jsx | 62 ++++++++----------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index 72394535..b23d8915 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -1,17 +1,17 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import TelemetryDashboard from '../components/TelemetryDashboard'; - -const mockAnalytics = { - getSummary: vi.fn(), - reset: vi.fn(), -}; vi.mock('../lib/analytics', () => ({ - analytics: mockAnalytics, + analytics: { + getSummary: vi.fn(), + reset: vi.fn(), + }, })); +import TelemetryDashboard from '../components/TelemetryDashboard'; +import { analytics } from '../lib/analytics'; + vi.mock('../context/DemoContext', () => ({ useDemoMode: vi.fn(() => ({ demoEnabled: false, @@ -77,7 +77,7 @@ describe('TelemetryDashboard error handling', () => { it('displays error state when analytics.getSummary throws', async () => { const errorMessage = 'Analytics service unavailable'; - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error(errorMessage); }); @@ -90,7 +90,7 @@ describe('TelemetryDashboard error handling', () => { }); it('shows retry button in error state', async () => { - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); }); @@ -103,7 +103,7 @@ describe('TelemetryDashboard error handling', () => { }); it('keeps export buttons available during error state', async () => { - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); }); @@ -119,7 +119,7 @@ describe('TelemetryDashboard error handling', () => { it('allows retry after error', async () => { const user = userEvent.setup(); - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); }); @@ -129,7 +129,7 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); }); - mockAnalytics.getSummary.mockReturnValue({ + analytics.getSummary.mockReturnValue({ sessions: 10, totalPageViews: 50, tipsConfirmed: 5, @@ -156,17 +156,18 @@ describe('TelemetryDashboard error handling', () => { }); it('clears error when data loads successfully', async () => { - mockAnalytics.getSummary.mockImplementation(() => { + const user = userEvent.setup(); + analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); }); - const { rerender } = render(); + render(); await waitFor(() => { expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); }); - mockAnalytics.getSummary.mockReturnValue({ + analytics.getSummary.mockReturnValue({ sessions: 10, totalPageViews: 50, tipsConfirmed: 5, @@ -184,7 +185,8 @@ describe('TelemetryDashboard error handling', () => { lastSeen: Date.now(), }); - rerender(); + const retryButton = screen.getByRole('button', { name: /retry/i }); + await user.click(retryButton); await waitFor(() => { expect(screen.queryByText('Failed to Load Telemetry Data')).not.toBeInTheDocument(); @@ -192,7 +194,7 @@ describe('TelemetryDashboard error handling', () => { }); it('displays generic error message for unknown errors', async () => { - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error(); }); @@ -205,7 +207,7 @@ describe('TelemetryDashboard error handling', () => { it('shows error with specific message from exception', async () => { const specificError = 'Database connection timeout'; - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error(specificError); }); @@ -217,7 +219,7 @@ describe('TelemetryDashboard error handling', () => { }); it('maintains error state across re-renders', async () => { - mockAnalytics.getSummary.mockImplementation(() => { + analytics.getSummary.mockImplementation(() => { throw new Error('Persistent error'); }); @@ -234,15 +236,8 @@ describe('TelemetryDashboard error handling', () => { it('exports JSON even when data load fails', async () => { const user = userEvent.setup(); - const mockDownloadExport = vi.fn(() => 'export.json'); - - vi.doMock('../lib/telemetry-export', () => ({ - downloadExport: mockDownloadExport, - exportToCsv: vi.fn(() => 'export.csv'), - copyToClipboard: vi.fn(), - })); - - mockAnalytics.getSummary.mockImplementation(() => { + + analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); }); @@ -263,15 +258,8 @@ describe('TelemetryDashboard error handling', () => { it('exports CSV even when data load fails', async () => { const user = userEvent.setup(); - const mockExportCsv = vi.fn(() => 'export.csv'); - - vi.doMock('../lib/telemetry-export', () => ({ - downloadExport: vi.fn(() => 'export.json'), - exportToCsv: mockExportCsv, - copyToClipboard: vi.fn(), - })); - - mockAnalytics.getSummary.mockImplementation(() => { + + analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); }); From df049c42010f8b4228695ef55d68f64b64cd041a Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Tue, 28 Apr 2026 20:55:59 +0100 Subject: [PATCH 04/22] Enhance error message display with monospace font and better styling --- frontend/src/components/TelemetryDashboard.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index 5a318939..1ee14f5c 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -185,10 +185,10 @@ export default function TelemetryDashboard({ addToast }) {

Failed to Load Telemetry Data

-

+

{error}

-
+
From 4381dfdeeba1fae895c8f700b9f7a369fc764d6e Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Tue, 28 Apr 2026 23:55:36 +0100 Subject: [PATCH 10/22] Add accessibility tests for error state ARIA attributes --- .../test/TelemetryDashboard.error.test.jsx | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index 195ac4be..fe82ab4a 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -110,8 +110,8 @@ describe('TelemetryDashboard error handling', () => { render(); await waitFor(() => { - const exportJsonButton = screen.getByRole('button', { name: /export json/i }); - const exportCsvButton = screen.getByRole('button', { name: /export csv/i }); + const exportJsonButton = screen.getByLabelText('Export telemetry data as JSON'); + const exportCsvButton = screen.getByLabelText('Export telemetry data as CSV'); expect(exportJsonButton).toBeInTheDocument(); expect(exportCsvButton).toBeInTheDocument(); }); @@ -248,7 +248,7 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); }); - const exportJsonButton = screen.getByRole('button', { name: /export json/i }); + const exportJsonButton = screen.getByLabelText('Export telemetry data as JSON'); await user.click(exportJsonButton); await waitFor(() => { @@ -270,7 +270,7 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); }); - const exportCsvButton = screen.getByRole('button', { name: /export csv/i }); + const exportCsvButton = screen.getByLabelText('Export telemetry data as CSV'); await user.click(exportCsvButton); await waitFor(() => { @@ -318,4 +318,32 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); }); }); + + it('error container has alert role for accessibility', async () => { + analytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + render(); + + await waitFor(() => { + const alertContainer = screen.getByRole('alert'); + expect(alertContainer).toBeInTheDocument(); + expect(alertContainer).toHaveAttribute('aria-live', 'assertive'); + }); + }); + + it('buttons have descriptive aria labels in error state', async () => { + analytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + render(); + + await waitFor(() => { + expect(screen.getByLabelText('Retry loading telemetry data')).toBeInTheDocument(); + expect(screen.getByLabelText('Export telemetry data as JSON')).toBeInTheDocument(); + expect(screen.getByLabelText('Export telemetry data as CSV')).toBeInTheDocument(); + }); + }); }); From 61086366ba506f3bb86c5bdb6a78bef35e158dce Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:01:12 +0100 Subject: [PATCH 11/22] Add test to verify export buttons remain enabled during error --- .../src/test/TelemetryDashboard.error.test.jsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index fe82ab4a..ce9bbe95 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -346,4 +346,19 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByLabelText('Export telemetry data as CSV')).toBeInTheDocument(); }); }); + + it('export buttons are enabled in error state', async () => { + analytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + render(); + + await waitFor(() => { + const exportJsonButton = screen.getByLabelText('Export telemetry data as JSON'); + const exportCsvButton = screen.getByLabelText('Export telemetry data as CSV'); + expect(exportJsonButton).not.toBeDisabled(); + expect(exportCsvButton).not.toBeDisabled(); + }); + }); }); From 8634397e7a003d18fb5914e91d7f8eee2adc99f1 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:01:51 +0100 Subject: [PATCH 12/22] Extract error message handling into separate function --- frontend/src/components/TelemetryDashboard.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index 4709f85a..5de28f1b 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -39,6 +39,15 @@ import { * at the top level to avoid "symbol has already been declared" errors during * Vite/Rollup transformation when build optimizations are enabled. */ + +/** + * Extract a user-friendly error message from an exception. + * @param {Error} err - The error object + * @returns {string} User-friendly error message + */ +function extractErrorMessage(err) { + return err?.message || 'Failed to load telemetry data'; +} export default function TelemetryDashboard({ addToast }) { const { demoEnabled } = useDemoMode(); const [summary, setSummary] = useState(null); @@ -92,7 +101,7 @@ export default function TelemetryDashboard({ addToast }) { setError(null); } catch (err) { // Extract error message for display, fallback to generic message - const errorMessage = err.message || 'Failed to load telemetry data'; + const errorMessage = extractErrorMessage(err); setError(errorMessage); // Keep console logging for debugging purposes console.error('Failed to load telemetry:', err); From 1ee26d2df06ce1c9353522f3356e902668b68436 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:02:30 +0100 Subject: [PATCH 13/22] Add JSDoc documentation for main component --- frontend/src/components/TelemetryDashboard.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index 5de28f1b..537bbdb2 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -48,6 +48,14 @@ import { function extractErrorMessage(err) { return err?.message || 'Failed to load telemetry data'; } + +/** + * TelemetryDashboard component displays application performance metrics. + * + * @param {Object} props - Component props + * @param {Function} props.addToast - Function to display toast notifications + * @returns {JSX.Element} The telemetry dashboard UI + */ export default function TelemetryDashboard({ addToast }) { const { demoEnabled } = useDemoMode(); const [summary, setSummary] = useState(null); From 13991ee8125faea0083672b7e728c4abb2aa6fc5 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:02:46 +0100 Subject: [PATCH 14/22] Improve error message extraction to handle string errors --- frontend/src/components/TelemetryDashboard.jsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index 537bbdb2..fd3fd983 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -46,7 +46,17 @@ import { * @returns {string} User-friendly error message */ function extractErrorMessage(err) { - return err?.message || 'Failed to load telemetry data'; + if (!err) return 'Failed to load telemetry data'; + + if (err.message) { + return err.message; + } + + if (typeof err === 'string') { + return err; + } + + return 'Failed to load telemetry data'; } /** From 3fd62acb110e49c2c4ba605ff9bd747ad84a052c Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:03:23 +0100 Subject: [PATCH 15/22] Add test for string error handling --- frontend/src/test/TelemetryDashboard.error.test.jsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index ce9bbe95..f6fcfb61 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -361,4 +361,17 @@ describe('TelemetryDashboard error handling', () => { expect(exportCsvButton).not.toBeDisabled(); }); }); + + it('handles string errors gracefully', async () => { + analytics.getSummary.mockImplementation(() => { + throw 'String error message'; + }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + expect(screen.getByText('String error message')).toBeInTheDocument(); + }); + }); }); From 9c47d2eebfe04d0b309e2b2eff503626f772ac77 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:03:39 +0100 Subject: [PATCH 16/22] Add comment explaining error state rendering --- frontend/src/components/TelemetryDashboard.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index fd3fd983..ddeb3237 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -210,6 +210,7 @@ export default function TelemetryDashboard({ addToast }) { ); } + // Render error state with retry and export options if (error) { return (
From d4fab56094e4700c61f7d6a17da8ef2df19f4a87 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:04:16 +0100 Subject: [PATCH 17/22] Add test to verify no error state on successful load --- .../test/TelemetryDashboard.error.test.jsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index f6fcfb61..d4a79249 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -374,4 +374,31 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('String error message')).toBeInTheDocument(); }); }); + + it('does not show error state when data loads successfully', async () => { + analytics.getSummary.mockReturnValue({ + sessions: 10, + totalPageViews: 50, + tipsConfirmed: 5, + tipCompletionRate: '50', + tipDropOffRate: '50', + batchTipsConfirmed: 1, + batchCompletionRate: '100', + totalErrors: 0, + sortedPages: [], + sortedErrors: [], + webVitals: {}, + walletConnections: 5, + walletDisconnections: 0, + firstSeen: Date.now(), + lastSeen: Date.now(), + }); + + render(); + + await waitFor(() => { + expect(screen.queryByText('Failed to Load Telemetry Data')).not.toBeInTheDocument(); + expect(screen.getByText('Telemetry Dashboard')).toBeInTheDocument(); + }); + }); }); From 04d7034f962d2933320b02edf851f310a23b5ee6 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:04:54 +0100 Subject: [PATCH 18/22] Add test for null error handling --- frontend/src/test/TelemetryDashboard.error.test.jsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index d4a79249..d0aba69b 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -401,4 +401,16 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('Telemetry Dashboard')).toBeInTheDocument(); }); }); + + it('handles null error with fallback message', async () => { + analytics.getSummary.mockImplementation(() => { + throw null; + }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + }); }); From cb7feb06b584a41fb891e05902198c88348c1b12 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:05:23 +0100 Subject: [PATCH 19/22] Add test to verify retry button triggers reload --- .../test/TelemetryDashboard.error.test.jsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index d0aba69b..c364d3bc 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -413,4 +413,47 @@ describe('TelemetryDashboard error handling', () => { expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); }); }); + + it('retry button triggers data reload', async () => { + const user = userEvent.setup(); + let callCount = 0; + + analytics.getSummary.mockImplementation(() => { + callCount++; + if (callCount === 1) { + throw new Error('First attempt failed'); + } + return { + sessions: 10, + totalPageViews: 50, + tipsConfirmed: 5, + tipCompletionRate: '50', + tipDropOffRate: '50', + batchTipsConfirmed: 1, + batchCompletionRate: '100', + totalErrors: 0, + sortedPages: [], + sortedErrors: [], + webVitals: {}, + walletConnections: 5, + walletDisconnections: 0, + firstSeen: Date.now(), + lastSeen: Date.now(), + }; + }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to Load Telemetry Data')).toBeInTheDocument(); + }); + + const retryButton = screen.getByLabelText('Retry loading telemetry data'); + await user.click(retryButton); + + await waitFor(() => { + expect(callCount).toBe(2); + expect(screen.queryByText('Failed to Load Telemetry Data')).not.toBeInTheDocument(); + }); + }); }); From ede8510e04be967ad5f2a9e3fa05954e1a6c41fd Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:06:09 +0100 Subject: [PATCH 20/22] Add test to verify error message container styling --- .../src/test/TelemetryDashboard.error.test.jsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index c364d3bc..48fa8e4a 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -456,4 +456,18 @@ describe('TelemetryDashboard error handling', () => { expect(screen.queryByText('Failed to Load Telemetry Data')).not.toBeInTheDocument(); }); }); + + it('error message container has proper styling', async () => { + analytics.getSummary.mockImplementation(() => { + throw new Error('Service error'); + }); + + const { container } = render(); + + await waitFor(() => { + const errorMessage = screen.getByText('Service error'); + expect(errorMessage.className).toContain('bg-gray-50'); + expect(errorMessage.className).toContain('border'); + }); + }); }); From 78ddd2fff88222f8544211a1018f47082651ed17 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:06:56 +0100 Subject: [PATCH 21/22] Extract default error message to constant --- frontend/src/components/TelemetryDashboard.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TelemetryDashboard.jsx b/frontend/src/components/TelemetryDashboard.jsx index ddeb3237..a7009565 100644 --- a/frontend/src/components/TelemetryDashboard.jsx +++ b/frontend/src/components/TelemetryDashboard.jsx @@ -40,13 +40,15 @@ import { * Vite/Rollup transformation when build optimizations are enabled. */ +const DEFAULT_ERROR_MESSAGE = 'Failed to load telemetry data'; + /** * Extract a user-friendly error message from an exception. * @param {Error} err - The error object * @returns {string} User-friendly error message */ function extractErrorMessage(err) { - if (!err) return 'Failed to load telemetry data'; + if (!err) return DEFAULT_ERROR_MESSAGE; if (err.message) { return err.message; @@ -56,7 +58,7 @@ function extractErrorMessage(err) { return err; } - return 'Failed to load telemetry data'; + return DEFAULT_ERROR_MESSAGE; } /** From 0af85b4ac55ab69dff33397cabe2871909dd20d8 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Wed, 29 Apr 2026 00:07:51 +0100 Subject: [PATCH 22/22] Organize tests with section comments --- frontend/src/test/TelemetryDashboard.error.test.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/test/TelemetryDashboard.error.test.jsx b/frontend/src/test/TelemetryDashboard.error.test.jsx index 48fa8e4a..4b415268 100644 --- a/frontend/src/test/TelemetryDashboard.error.test.jsx +++ b/frontend/src/test/TelemetryDashboard.error.test.jsx @@ -75,6 +75,7 @@ describe('TelemetryDashboard error handling', () => { vi.clearAllMocks(); }); + // Basic error display tests it('displays error state when analytics.getSummary throws', async () => { const errorMessage = 'Analytics service unavailable'; analytics.getSummary.mockImplementation(() => { @@ -102,6 +103,7 @@ describe('TelemetryDashboard error handling', () => { }); }); + // Button availability tests it('keeps export buttons available during error state', async () => { analytics.getSummary.mockImplementation(() => { throw new Error('Service error'); @@ -117,6 +119,7 @@ describe('TelemetryDashboard error handling', () => { }); }); + // Retry functionality tests it('allows retry after error', async () => { const user = userEvent.setup(); analytics.getSummary.mockImplementation(() => { @@ -193,6 +196,7 @@ describe('TelemetryDashboard error handling', () => { }); }); + // Error message handling tests it('displays generic error message for unknown errors', async () => { analytics.getSummary.mockImplementation(() => { throw new Error();