1// @vitest-environment jsdom
2
3import { FunctionalComponent as FC, h } from 'preact';
4import { render, cleanup, act } from '@testing-library/preact';
5import { OperationContext } from '@urql/core';
6import {
7 vi,
8 expect,
9 it,
10 beforeEach,
11 describe,
12 beforeAll,
13 Mock,
14 afterEach,
15} from 'vitest';
16import { merge, fromValue, never, empty } from 'wonka';
17
18import { useSubscription, UseSubscriptionState } from './useSubscription';
19import { Provider } from '../context';
20
21const data = { data: 1234, error: 5678 };
22const mock = {
23 // @ts-ignore
24 executeSubscription: vi.fn(() => merge([fromValue(data), never])),
25};
26
27const client = mock as { executeSubscription: Mock };
28const query = 'subscription Example { example }';
29
30let state: UseSubscriptionState<any> | undefined;
31let execute: ((_opts?: Partial<OperationContext>) => void) | undefined;
32
33const SubscriptionUser: FC<{
34 q: string;
35 handler?: (_prev: any, _data: any) => any;
36 context?: Partial<OperationContext>;
37 pause?: boolean;
38}> = ({ q, handler, context, pause = false }) => {
39 [state, execute] = useSubscription({ query: q, context, pause }, handler);
40 return h('p', {}, state.data);
41};
42
43beforeAll(() => {
44 vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
45 // do nothing
46 });
47});
48
49describe('useSubscription', () => {
50 beforeEach(() => {
51 client.executeSubscription.mockClear();
52 state = undefined;
53 execute = undefined;
54 });
55
56 const props = { q: query };
57
58 afterEach(() => cleanup());
59
60 it('executes subscription', () => {
61 render(
62 h(Provider, {
63 value: client as any,
64 children: [h(SubscriptionUser, { ...props })],
65 })
66 );
67 expect(client.executeSubscription).toBeCalledTimes(1);
68 });
69
70 it('should support setting context in useSubscription params', () => {
71 render(
72 h(Provider, {
73 value: client as any,
74 children: [h(SubscriptionUser, { ...props, context: { url: 'test' } })],
75 })
76 );
77 expect(client.executeSubscription).toBeCalledWith(
78 {
79 key: expect.any(Number),
80 query: expect.any(Object),
81 variables: {},
82 },
83 {
84 url: 'test',
85 }
86 );
87 });
88
89 describe('on subscription', () => {
90 it('forwards client response', () => {
91 const { rerender } = render(
92 h(Provider, {
93 value: client as any,
94 children: [h(SubscriptionUser, { ...props })],
95 })
96 );
97 /**
98 * Have to call update (without changes) in order to see the
99 * result of the state change.
100 */
101 rerender(
102 h(Provider, {
103 value: client as any,
104 children: [h(SubscriptionUser, { ...props })],
105 })
106 );
107 expect(state).toEqual({
108 ...data,
109 hasNext: false,
110 extensions: undefined,
111 fetching: true,
112 stale: false,
113 });
114 });
115 });
116
117 it('calls handler', () => {
118 const handler = vi.fn();
119 const { rerender } = render(
120 h(Provider, {
121 value: client as any,
122 children: [h(SubscriptionUser, { ...props, handler })],
123 })
124 );
125 rerender(
126 h(Provider, {
127 value: client as any,
128 children: [h(SubscriptionUser, { ...props })],
129 })
130 );
131 expect(handler).toBeCalledTimes(2);
132 expect(handler).toBeCalledWith(undefined, 1234);
133 });
134
135 describe('active teardown', () => {
136 it('sets fetching to false when the source ends', () => {
137 client.executeSubscription.mockReturnValueOnce(empty);
138 render(
139 h(Provider, {
140 value: client as any,
141 children: [h(SubscriptionUser, { ...props })],
142 })
143 );
144 expect(client.executeSubscription).toHaveBeenCalled();
145 expect(state).toMatchObject({ fetching: false });
146 });
147 });
148
149 describe('execute subscription', () => {
150 it('triggers subscription execution', () => {
151 render(
152 h(Provider, {
153 value: client as any,
154 children: [h(SubscriptionUser, { ...props })],
155 })
156 );
157 act(() => execute && execute());
158 expect(client.executeSubscription).toBeCalledTimes(2);
159 });
160 });
161
162 describe('pause', () => {
163 const props = { q: query };
164
165 it('skips executing the query if pause is true', () => {
166 render(
167 h(Provider, {
168 value: client as any,
169 children: [h(SubscriptionUser, { ...props, pause: true })],
170 })
171 );
172 expect(client.executeSubscription).not.toBeCalled();
173 });
174
175 it('skips executing queries if pause updates to true', () => {
176 const { rerender } = render(
177 h(Provider, {
178 value: client as any,
179 children: [h(SubscriptionUser, { ...props })],
180 })
181 );
182
183 rerender(
184 h(Provider, {
185 value: client as any,
186 children: [h(SubscriptionUser, { ...props, pause: true })],
187 })
188 );
189 rerender(
190 h(Provider, {
191 value: client as any,
192 children: [h(SubscriptionUser, { ...props, pause: true })],
193 })
194 );
195 expect(client.executeSubscription).toBeCalledTimes(1);
196 expect(state).toMatchObject({ fetching: false });
197 });
198 });
199});