diff --git a/src/renderer/src/components/HttpCaptureStep.tsx b/src/renderer/src/components/HttpCaptureStep.tsx index 657e73c3..854d4150 100644 --- a/src/renderer/src/components/HttpCaptureStep.tsx +++ b/src/renderer/src/components/HttpCaptureStep.tsx @@ -160,6 +160,12 @@ export function HttpCaptureStep({ onVerified, onPrepare, paused = false }: { useEffect(() => { onVerified?.(verified) }, [verified, onVerified]) + // A rejected start is this card's to report until the proxy says something + // newer. A status read that finds it merely stopped knows nothing about why, + // and taking it whole erased the failure within one 3 s poll. + const takeStatus = (st: ManagedProxyStatus): void => + setStatus((prev) => (prev.state === 'failed' && prev.error && st.state === 'stopped' ? prev : st)) + const check = async (): Promise => { const [pf, st, cfg] = await Promise.all([ window.redlog.runtime.preflight().catch(() => null), @@ -172,7 +178,7 @@ export function HttpCaptureStep({ onVerified, onPrepare, paused = false }: { const mitm = pf.checks.find((c) => c.id === 'mitmdump') setMitmMissing(mitm ? !mitm.found : false) } - if (st) setStatus(st) + if (st) takeStatus(st) if (cfg) setConfig(cfg as Record) } @@ -192,7 +198,7 @@ export function HttpCaptureStep({ onVerified, onPrepare, paused = false }: { // request. Same cadence as the app-wide toggle. useEffect(() => { const timer = setInterval(() => { - window.redlog.httpCapture.status().then(setStatus).catch(() => { /* keep the last known state */ }) + window.redlog.httpCapture.status().then(takeStatus).catch(() => { /* keep the last known state */ }) }, 3_000) return () => clearInterval(timer) }, []) diff --git a/test/first-run-record-terminal.test.tsx b/test/first-run-record-terminal.test.tsx index 3ef0648c..e010535b 100644 --- a/test/first-run-record-terminal.test.tsx +++ b/test/first-run-record-terminal.test.tsx @@ -488,6 +488,24 @@ describe('first run: HTTP(S) card reports its own failures (UI/UX audit F6/F7)', expect(screen.getByTestId('first-run-http-restart')).not.toBeNull() }) + // The card also polls the proxy's status every 3 s. The poll's answer knows + // nothing about this start, so taking it whole erased the failure: on a + // slow runner the poll landed before the assertion above, and the operator + // saw the message for at most three seconds. + it('keeps a rejected start\'s failure through the next status poll', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + install() + bridge.proxyStart.mockRejectedValueOnce(new Error('port 8080 in use')) + draw() + fireEvent.click(await screen.findByRole('button', { name: '重新啟動 HTTP 擷取' }, { timeout: 5000 })) + const card = await screen.findByTestId('first-run-http') + await waitFor(() => expect(card.textContent).toContain('port 8080 in use')) + const polls = bridge.proxyStatus.mock.calls.length + await act(async () => { await vi.advanceTimersByTimeAsync(3_100) }) + expect(bridge.proxyStatus.mock.calls.length).toBeGreaterThan(polls) + expect(card.textContent).toContain('port 8080 in use') + }) + // The terminal-proxy switch moved to Settings ▸ Browser, which owns every // other proxy setting; a global behaviour switch does not belong on the // screen where capture is being proved for the first time. diff --git a/test/terminal-session-rotation.test.ts b/test/terminal-session-rotation.test.ts index aa54fbea..53f2c049 100644 --- a/test/terminal-session-rotation.test.ts +++ b/test/terminal-session-rotation.test.ts @@ -142,7 +142,10 @@ describe('project recording rotation without killing the pane', () => { expect(detachTerminalRecordings()).toBe(10) closeDB(); resetCausesResolver(); current = 'B'; open('B') expect(resumeTerminalRecordings()).toBe(10) - expect(performance.now() - started).toBeLessThan(1000) + // A rotation that waited would wait for `sleep 99`. Five seconds still + // tells that apart, where one second failed on a loaded Windows runner + // (1205 ms) while the rotation itself did nothing wrong. + expect(performance.now() - started).toBeLessThan(5000) expect(listTerminals()).toHaveLength(10) expect(fake.panes.every(pane => pane.kill.mock.calls.length === 0)).toBe(true) expect(rows().filter(row => row.data.subtype === 'session_start').every(row => row.data.rotationPaneCount === 10)).toBe(true)