// @vitest-environment jsdom import { FunctionalComponent as FC, h } from 'preact'; import { render, cleanup, act } from '@testing-library/preact'; import { OperationContext } from '@urql/core'; import { map, interval, pipe, never, onStart, onEnd, empty } from 'wonka'; import { vi, expect, it, beforeEach, describe, afterEach, Mock } from 'vitest'; import { useQuery, UseQueryArgs, UseQueryState } from './useQuery'; import { Provider } from '../context'; const mock = { executeQuery: vi.fn(() => pipe( interval(400), map((i: number) => ({ data: i, error: i + 1, extensions: { i: 1 } })) ) ), }; const client = mock as { executeQuery: Mock }; const props: UseQueryArgs<{ myVar: number }> = { query: '{ example }', variables: { myVar: 1234, }, pause: false, }; let state: UseQueryState | undefined; let execute: ((_opts?: Partial) => void) | undefined; const QueryUser: FC> = ({ query, variables, pause, }) => { [state, execute] = useQuery({ query, variables, pause }); return h('p', {}, state.data); }; beforeEach(() => { vi.useFakeTimers(); vi.spyOn(globalThis.console, 'error'); }); describe('useQuery', () => { beforeEach(() => { client.executeQuery.mockClear(); state = undefined; execute = undefined; }); afterEach(() => cleanup()); it('executes subscription', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); expect(client.executeQuery).toBeCalledTimes(1); }); it('passes query and vars to executeQuery', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); expect(client.executeQuery).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: props.variables, }, expect.objectContaining({ requestPolicy: undefined, }) ); }); it('sets fetching to true', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); expect(state).toHaveProperty('fetching', true); }); it('forwards data response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('data', 0); }); it('forwards error response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('error', 1); }); it('forwards extensions response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('extensions', { i: 1 }); }); it('sets fetching to false', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('fetching', false); }); describe('on change', () => { const q = 'query NewQuery { example }'; it('new query executes subscription', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, query: q })], }) ); act(() => { rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, query: q })], }) ); }); expect(client.executeQuery).toBeCalledTimes(2); }); }); describe('on unmount', () => { const start = vi.fn(); const unsubscribe = vi.fn(); beforeEach(() => { client.executeQuery.mockReturnValue( pipe(never, onStart(start), onEnd(unsubscribe)) ); }); it('unsubscribe is called', () => { const { unmount } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { unmount(); }); expect(start).toBeCalledTimes(2); expect(unsubscribe).toBeCalledTimes(2); }); }); describe('active teardown', () => { it('sets fetching to false when the source ends', () => { client.executeQuery.mockReturnValueOnce(empty); act(() => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(client.executeQuery).toHaveBeenCalled(); expect(state).toMatchObject({ fetching: false }); }); }); describe('execute query', () => { it('triggers query execution', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => execute && execute()); expect(client.executeQuery).toBeCalledTimes(2); }); }); describe('pause', () => { it('skips executing the query if pause is true', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, pause: true })], }) ); expect(client.executeQuery).not.toBeCalled(); }); it('skips executing queries if pause updates to true', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, pause: true })], }) ); expect(client.executeQuery).toBeCalledTimes(1); }); }); });