diff --git a/packages/react/tests/unit/context7-widget.test.tsx b/packages/react/tests/unit/context7-widget.test.tsx index 261393a..28dc02c 100644 --- a/packages/react/tests/unit/context7-widget.test.tsx +++ b/packages/react/tests/unit/context7-widget.test.tsx @@ -452,6 +452,35 @@ describe('@desource/context7-widget-react', () => { expect(container.querySelector('.context7-widget')?.hasAttribute('open')).toBe(false); }); + it('renders the localized fallback when a stream error has no message', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.error(new Error('')); + } + }); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(stream))); + const controllerRef: { current: Context7WidgetHandle | null } = { current: null }; + const container = mount( + { + controllerRef.current = value; + }} + labels={{ errorFallback: 'Documentation is temporarily unavailable.' }} + library="/owner/repo" + /> + ); + const controller = required(controllerRef.current, 'Expected a React widget controller.'); + + await act(async () => { + expect(await controller.send('Find the documentation')).toMatchObject({ error: '', status: 'error' }); + }); + + expect(container.querySelector('.c7-message--error')?.textContent).toContain( + 'Documentation is temporarily unavailable.' + ); + expect(controller.isBusy()).toBe(false); + }); + it('handles composition, keyboard submission, cancellation, and modal focus wrapping', async () => { const deferred = createDeferredSseStream(); const fetchMock = vi.fn(async () => new Response(deferred.stream)); @@ -500,6 +529,15 @@ describe('@desource/context7-widget-react', () => { expect(submitEvent.defaultPrevented).toBe(true); await vi.waitFor(() => expect(controller.isBusy()).toBe(true)); + const stop = required(container.querySelector('.c7-send'), 'Expected the Stop control.'); + stop.focus(); + await act(async () => { + await controller.retry(); + }); + expect(controller.isBusy()).toBe(true); + expect(document.activeElement).toBe(stop); + expect(fetchMock).toHaveBeenCalledOnce(); + await act(async () => container.querySelector('.c7-send')?.click()); await vi.waitFor(() => expect(controller.isBusy()).toBe(false)); await act(async () => { @@ -1066,6 +1104,33 @@ describe('@desource/context7-widget-react', () => { expect(secondTarget.querySelector('.context7-widget-programmatic-root')).toBeNull(); }); + it('does not recreate an owned widget after unmounting before a queued prop update', async () => { + const controlsRef: { current: ReturnType | null } = { current: null }; + let setPreset: ((value: 'default' | 'glass') => void) | null = null; + + function Harness() { + const [preset, updatePreset] = useState<'default' | 'glass'>('default'); + setPreset = updatePreset; + controlsRef.current = useContext7Widget({ library: '/owner/repo', preset }); + return null; + } + + mount(); + const controls = required(controlsRef.current, 'Expected hook controls.'); + await act(async () => { + controls.mount(); + }); + expect(document.querySelector('.context7-widget-programmatic-root')).not.toBeNull(); + + act(() => setPreset?.('glass')); + act(() => controls.unmount()); + await flush(); + + expect(document.querySelector('.context7-widget-programmatic-root')).toBeNull(); + expect(controlsRef.current?.widget).toBeNull(); + expect(controlsRef.current?.isOpen).toBe(false); + }); + it('cancels a queued auto-mount when the hook owner unmounts first', async () => { const ownerContainer = document.createElement('div'); const widgetTarget = document.createElement('div');