diff --git a/src/cli.js b/src/cli.js index 7c588fcc..f4847531 100644 --- a/src/cli.js +++ b/src/cli.js @@ -430,25 +430,50 @@ tddCmd globalOptions ); - // Set up cleanup on process signals + // Track cleanup state to prevent double cleanup + let isCleaningUp = false; const handleCleanup = async () => { + if (isCleaningUp) return; + isCleaningUp = true; await cleanup(); }; - process.once('SIGINT', () => { - handleCleanup().then(() => process.exit(1)); - }); + // Set up cleanup on process signals + const sigintHandler = () => { + handleCleanup().then(() => process.exit(result?.exitCode || 0)); + }; + const sigtermHandler = () => { + handleCleanup().then(() => process.exit(result?.exitCode || 0)); + }; - process.once('SIGTERM', () => { - handleCleanup().then(() => process.exit(1)); - }); + process.once('SIGINT', sigintHandler); + process.once('SIGTERM', sigtermHandler); - if (result && !result.success && result.exitCode > 0) { - await cleanup(); - process.exit(result.exitCode); + // If there are comparisons, keep server running for review + const hasComparisons = result?.comparisons?.length > 0; + if (hasComparisons) { + output.print( + ` ${colors.brand.textTertiary('→')} Press ${colors.white('Enter')} to stop server` + ); + output.blank(); + + // Wait for user to press Enter + await new Promise(resolve => { + process.stdin.setRawMode?.(false); + process.stdin.resume(); + process.stdin.once('data', () => { + process.stdin.pause(); + resolve(); + }); + }); } - await cleanup(); + // Remove signal handlers before normal cleanup to prevent double cleanup + process.off('SIGINT', sigintHandler); + process.off('SIGTERM', sigtermHandler); + + await handleCleanup(); + process.exit(result?.exitCode || 0); }); program diff --git a/src/reporter/src/api/client.js b/src/reporter/src/api/client.js index b4ce7991..44f51402 100644 --- a/src/reporter/src/api/client.js +++ b/src/reporter/src/api/client.js @@ -53,6 +53,10 @@ export const tdd = { * @returns {Promise} */ async getReportData() { + // In static mode, return embedded data directly without fetching + if (isStaticMode() && window.VIZZLY_REPORTER_DATA) { + return window.VIZZLY_REPORTER_DATA; + } return fetchJson('/api/report-data'); }, diff --git a/src/reporter/src/hooks/queries/use-tdd-queries.js b/src/reporter/src/hooks/queries/use-tdd-queries.js index 7632d3b9..87b736f6 100644 --- a/src/reporter/src/hooks/queries/use-tdd-queries.js +++ b/src/reporter/src/hooks/queries/use-tdd-queries.js @@ -1,12 +1,21 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { tdd } from '../../api/client.js'; +import { useMemo } from 'react'; +import { isStaticMode, tdd } from '../../api/client.js'; import { queryKeys } from '../../lib/query-keys.js'; import { SSE_STATE, useReportDataSSE } from '../use-sse.js'; export function useReportData(options = {}) { + // Check if we're in static mode with embedded data + // Memoize to ensure consistency within the hook's lifecycle + const staticMode = useMemo(() => isStaticMode(), []); + const staticData = useMemo( + () => (staticMode ? window.VIZZLY_REPORTER_DATA : undefined), + [staticMode] + ); + // Use SSE for real-time updates (handles static mode internally) const { state: sseState } = useReportDataSSE({ - enabled: options.polling !== false, + enabled: options.polling !== false && !staticMode, }); // SSE is connected - it updates the cache directly, no polling needed @@ -16,8 +25,14 @@ export function useReportData(options = {}) { return useQuery({ queryKey: queryKeys.reportData(), queryFn: tdd.getReportData, - // Only poll as fallback when SSE is not connected - refetchInterval: options.polling !== false && !sseConnected ? 2000 : false, + // In static mode, provide initial data and disable refetching + initialData: staticData, + // Only poll as fallback when SSE is not connected and not in static mode + refetchInterval: + !staticMode && options.polling !== false && !sseConnected ? 2000 : false, + // Don't refetch in static mode + refetchOnMount: !staticMode, + refetchOnWindowFocus: !staticMode, ...options, }); }