import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/react' import SupportHQWidget from './support-hq-widget' const useSessionMock = vi.hoisted(() => vi.fn()) vi.mock('next-auth/react', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useSession: useSessionMock } }) const init = vi.fn() const destroy = vi.fn() // A fresh object every call, the way SessionProvider hands one back after a // refetch even when the underlying user has not changed. const sessionWith = (overrides: Record = {}) => ({ data: { user: { id: 'user_1', name: 'Test User', email: 'test@example.com', phone: '', ...overrides, }, }, }) const scripts = () => document.querySelectorAll('script[src*="supporthq-widget.js"]') // jsdom does not fetch external scripts, so loading is simulated. const fireLoad = () => scripts().forEach((s) => (s as HTMLScriptElement).onload?.(new Event('load'))) beforeEach(() => { init.mockClear() destroy.mockClear() // @ts-ignore window.SupportHQWidget = { init, destroy } }) afterEach(() => { document .querySelectorAll('script[src*="supporthq-widget.js"]') .forEach((s) => s.remove()) }) describe('SupportHQWidget', () => { it('injects the widget script once and initialises with the user metadata', () => { useSessionMock.mockReturnValue(sessionWith()) render() expect(scripts()).toHaveLength(1) fireLoad() expect(init).toHaveBeenCalledTimes(1) expect(init.mock.calls[0][0].metadata).toEqual({ userId: 'user_1', name: 'Test User', email: 'test@example.com', phone: '', }) }) // The reported defect: the effect depended on the session object, so a // refetch that returned identical data still destroyed and re-injected the // widget, closing any chat the user had open. it('does not reinitialise when a refetch returns identical data', () => { useSessionMock.mockReturnValue(sessionWith()) const { rerender } = render() fireLoad() useSessionMock.mockReturnValue(sessionWith()) rerender() expect(scripts()).toHaveLength(1) expect(destroy).not.toHaveBeenCalled() expect(init).toHaveBeenCalledTimes(1) }) it('reinitialises when the user metadata actually changes', () => { useSessionMock.mockReturnValue(sessionWith()) const { rerender } = render() fireLoad() useSessionMock.mockReturnValue(sessionWith({ email: 'new@example.com' })) rerender() fireLoad() expect(destroy).toHaveBeenCalledTimes(1) expect(init).toHaveBeenCalledTimes(2) expect(init.mock.calls[1][0].metadata.email).toBe('new@example.com') }) // destroy() tears down the widget but leaves the tag, so without an explicit // removal every re-run left another one behind. it('leaves no script tags behind when it unmounts', () => { useSessionMock.mockReturnValue(sessionWith()) const { unmount } = render() fireLoad() unmount() expect(scripts()).toHaveLength(0) expect(destroy).toHaveBeenCalledTimes(1) }) it('does not initialise if the script loads after cleanup', () => { useSessionMock.mockReturnValue(sessionWith()) const { unmount } = render() const pending = scripts()[0] as HTMLScriptElement unmount() // A cached src can resolve after the effect was torn down; initialising // then would leave a widget that nothing destroys. pending.onload?.(new Event('load')) expect(init).not.toHaveBeenCalled() }) it('omits metadata when nobody is signed in', () => { useSessionMock.mockReturnValue({ data: null }) render() fireLoad() expect(init).toHaveBeenCalledTimes(1) expect(init.mock.calls[0][0]).not.toHaveProperty('metadata') }) })