Repository: FormidableLabs/urql
Pull request #1153 (2020-11-14 13:13:47)

(vue-urql) - Add Tests
<!--
  Thanks for opening a pull request! We appreciate your dedication and help!
  Before submitting your pull request, please make sure to read our CONTRIBUTING guide.

  The best contribution is always a PR! However, if you're starting to work on a large
  change, it's best to make sure to open an issue first.

  If this PR is already related to an issue, please reference it like so:
  Resolves #123
-->

## Summary

This PR adds tests for:
- `useMutation`
- `useQuery`
- `useSubscription`

## Set of changes

- The added tests cover the same cases as the svelte implementation.
- I had to add `jest` to the package's tsconfig type option in order to stop TS from complaining, this should not have any impact outside of the vue-urql package.
- This is a draft PR for now as I came across some problems to test `useQuery().then()` behavior, and I'm not certain what the desired behavior here should be. Please see the code comments 

This only adds tests, so I didn't create a changeset. I can provide one if that's still required for this kind of PR. 

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: f31f99be112631c6e3b45139bc850a4b91c3896e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/LinusBorg/urql/new/vue-urql-tests?filename=.changeset/two-gifts-cry.md&value=---%0A%22%40urql%2Fvue%22%3A%20patch%0A---%0A%0A(vue-urql)%20-%20Add%20Tests%0A)



Patch:
diff --git a/.changeset/flat-rats-fetch.md b/.changeset/flat-rats-fetch.md
new file mode 100644
index 00000000..7dfd86cb
--- /dev/null
+++ b/.changeset/flat-rats-fetch.md
@@ -0,0 +1,5 @@
+---
+'@urql/vue': patch
+---
+
+fix pausing feature of useQuery by turning "isPaused" into a ref again.
diff --git a/.changeset/odd-starfishes-punch.md b/.changeset/odd-starfishes-punch.md
new file mode 100644
index 00000000..69795d24
--- /dev/null
+++ b/.changeset/odd-starfishes-punch.md
@@ -0,0 +1,5 @@
+---
+'@urql/vue': minor
+---
+
+expose Vue plugin function as default export
diff --git a/packages/vue-urql/src/index.ts b/packages/vue-urql/src/index.ts
index 1b12832b..a716ee89 100644
--- a/packages/vue-urql/src/index.ts
+++ b/packages/vue-urql/src/index.ts
@@ -3,3 +3,6 @@ export * from './useClient';
 export * from './useQuery';
 export * from './useMutation';
 export * from './useSubscription';
+import { install } from './useClient';
+
+export default install;
diff --git a/packages/vue-urql/src/useClient.ts b/packages/vue-urql/src/useClient.ts
index ca92ff79..e088115f 100644
--- a/packages/vue-urql/src/useClient.ts
+++ b/packages/vue-urql/src/useClient.ts
@@ -1,4 +1,4 @@
-import { inject, provide } from 'vue';
+import { App, inject, provide } from 'vue';
 import { Client, ClientOptions } from '@urql/core';
 
 export function provideClient(opts: ClientOptions | Client) {
@@ -7,11 +7,16 @@ export function provideClient(opts: ClientOptions | Client) {
   return client;
 }
 
+export function install(app: App, opts: ClientOptions | Client) {
+  const client = opts instanceof Client ? opts : new Client(opts);
+  app.provide('$urql', client);
+}
+
 export function useClient(): Client {
   const client = inject('$urql') as Client;
   if (process.env.NODE_ENV !== 'production' && !client) {
     throw new Error(
-      'No urql Client was provided. Did you forget to call `provideClient` in a parent?'
+      'No urql Client was provided. Did you forget to install the plugin or call `provideClient` in a parent?'
     );
   }
 
diff --git a/packages/vue-urql/src/useQuery.ts b/packages/vue-urql/src/useQuery.ts
index 1bd818f6..69041c78 100644
--- a/packages/vue-urql/src/useQuery.ts
+++ b/packages/vue-urql/src/useQuery.ts
@@ -1,4 +1,4 @@
-import { Ref, unref, ref, isRef, watchEffect } from 'vue';
+import { Ref, ref, watchEffect, reactive, isRef } from 'vue';
 import { DocumentNode } from 'graphql';
 import { pipe, take, publish, share, onPush, toPromise, onEnd } from 'wonka';
 
@@ -45,8 +45,9 @@ export type UseQueryResponse<T = any, V = object> = UseQueryState<T, V> &
   PromiseLike<UseQueryState<T, V>>;
 
 export function useQuery<T = any, V = object>(
-  args: UseQueryArgs<T, V>
+  _args: UseQueryArgs<T, V>
 ): UseQueryResponse<T, V> {
+  const args = reactive(_args);
   const client = useClient();
 
   const data: Ref<T | undefined> = ref();
@@ -60,20 +61,17 @@ export function useQuery<T = any, V = object>(
     /* noop */
   });
 
-  const isPaused: Ref<boolean> = isRef(args.pause)
-    ? args.pause
-    : ref(!!args.pause);
+  const isPaused: Ref<boolean> = isRef(_args.pause)
+    ? _args.pause
+    : ref(!!_args.pause);
 
   const request: Ref<GraphQLRequest<T, V>> = ref(
-    createRequest<T, V>(unref(args.query), unref(args.variables) as V) as any
+    createRequest<T, V>(args.query, args.variables as V) as any
   );
 
   watchEffect(
     () => {
-      const newRequest = createRequest<T, V>(
-        unref(args.query),
-        unref(args.variables) as any
-      );
+      const newRequest = createRequest<T, V>(args.query, args.variables as any);
       if (request.value.key !== newRequest.key) {
         request.value = newRequest;
       }
@@ -90,9 +88,9 @@ export function useQuery<T = any, V = object>(
 
     const query$ = pipe(
       client.executeQuery<T, V>(request.value, {
-        requestPolicy: unref(args.requestPolicy),
-        pollInterval: unref(args.pollInterval),
-        ...unref(args.context),
+        requestPolicy: args.requestPolicy,
+        pollInterval: args.pollInterval,
+        ...args.context,
         ...opts,
       }),
       onEnd(() => {
diff --git a/packages/vue-urql/src/useSubscription.ts b/packages/vue-urql/src/useSubscription.ts
index a54ef27e..661faa2b 100644
--- a/packages/vue-urql/src/useSubscription.ts
+++ b/packages/vue-urql/src/useSubscription.ts
@@ -1,4 +1,4 @@
-import { Ref, unref, ref, isRef, watchEffect } from 'vue';
+import { Ref, ref, watchEffect, reactive, isRef } from 'vue';
 import { DocumentNode } from 'graphql';
 import { pipe, subscribe, onEnd } from 'wonka';
 
@@ -44,9 +44,11 @@ export type UseSubscriptionResponse<
 > = UseSubscriptionState<T, R, V>;
 
 export function useSubscription<T = any, R = T, V = object>(
-  args: UseSubscriptionArgs<T, V>,
+  _args: UseSubscriptionArgs<T, V>,
   handler?: MaybeRef<SubscriptionHandler<T, R>>
 ): UseSubscriptionResponse<T, R, V> {
+  const args = reactive(_args);
+
   const client = useClient();
 
   const data: Ref<R | undefined> = ref();
@@ -60,24 +62,19 @@ export function useSubscription<T = any, R = T, V = object>(
     /* noop */
   });
 
-  const scanHandler: Ref<SubscriptionHandler<T, R> | undefined> = isRef(handler)
-    ? handler
-    : ref(handler);
+  const scanHandler: Ref<SubscriptionHandler<T, R> | undefined> = ref(handler);
 
-  const isPaused: Ref<boolean> = isRef(args.pause)
-    ? args.pause
-    : ref(!!args.pause);
+  const isPaused: Ref<boolean> = isRef(_args.pause)
+    ? _args.pause
+    : ref(!!_args.pause);
 
   const request: Ref<GraphQLRequest<T, V>> = ref(
-    createRequest<T, V>(unref(args.query), unref(args.variables) as V) as any
+    createRequest<T, V>(args.query, args.variables as V) as any
   );
 
   watchEffect(
     () => {
-      const newRequest = createRequest<T, V>(
-        unref(args.query),
-        unref(args.variables) as any
-      );
+      const newRequest = createRequest<T, V>(args.query, args.variables as any);
       if (request.value.key !== newRequest.key) {
         request.value = newRequest;
       }
@@ -93,7 +90,7 @@ export function useSubscription<T = any, R = T, V = object>(
     unsubscribe.value();
     unsubscribe.value = pipe(
       client.executeSubscription<T, V>(request.value, {
-        ...unref(args.context),
+        ...args.context,
         ...opts,
       }),
       onEnd(() => {
diff --git a/packages/vue-urql/tsconfig.json b/packages/vue-urql/tsconfig.json
index 1df51497..218c6fe1 100644
--- a/packages/vue-urql/tsconfig.json
+++ b/packages/vue-urql/tsconfig.json
@@ -3,7 +3,7 @@
   "include": ["src"],
   "compilerOptions": {
     "baseUrl": "./",
-    "types": ["node", "vue"],
+    "types": ["node", "vue", "jest"],
     "paths": {
       "urql": ["../../node_modules/urql/src"],
       "*-urql": ["../../node_modules/*-urql/src"],


Test patch:
diff --git a/packages/vue-urql/src/useMutation.test.ts b/packages/vue-urql/src/useMutation.test.ts
new file mode 100644
index 00000000..d4e0897d
--- /dev/null
+++ b/packages/vue-urql/src/useMutation.test.ts
@@ -0,0 +1,60 @@
+jest.mock('vue', () => {
+  const vue = jest.requireActual('vue');
+
+  return {
+    __esModule: true,
+    ...vue,
+    inject: () => client,
+  };
+});
+import { makeSubject, pipe, take, toPromise } from 'wonka';
+import { createClient } from '@urql/core';
+import { useMutation } from './useMutation';
+import { reactive } from 'vue';
+
+const client = createClient({ url: '/graphql', exchanges: [] });
+
+beforeEach(() => {
+  jest.resetAllMocks();
+});
+
+describe('useMutation', () => {
+  it('provides an execute method that resolves a promise', async () => {
+    const subject = makeSubject<any>();
+    const clientMutation = jest
+      .spyOn(client, 'mutation')
+      .mockImplementation((): any => ({
+        toPromise() {
+          return pipe(subject.source, take(1), toPromise);
+        },
+      }));
+    const mutation = reactive(useMutation('mutation { test }'));
+
+    expect(mutation).toMatchObject({
+      data: undefined,
+      stale: false,
+      fetching: false,
+      error: undefined,
+      extensions: undefined,
+      operation: undefined,
+      executeMutation: expect.any(Function),
+    });
+
+    const promise = mutation.executeMutation({ test: true });
+
+    expect(mutation.fetching).toBe(true);
+    expect(mutation.stale).toBe(false);
+    expect(mutation.error).toBe(undefined);
+
+    expect(clientMutation).toHaveBeenCalledTimes(1);
+
+    subject.next({ data: { test: true } });
+
+    await promise;
+
+    expect(mutation.fetching).toBe(false);
+    expect(mutation.stale).toBe(false);
+    expect(mutation.error).toBe(undefined);
+    expect(mutation.data).toEqual({ test: true });
+  });
+});
diff --git a/packages/vue-urql/src/useQuery.test.ts b/packages/vue-urql/src/useQuery.test.ts
new file mode 100644
index 00000000..f7bdaeac
--- /dev/null
+++ b/packages/vue-urql/src/useQuery.test.ts
@@ -0,0 +1,156 @@
+jest.mock('vue', () => {
+  const vue = jest.requireActual('vue');
+
+  return {
+    __esModule: true,
+    ...vue,
+    inject: () => client,
+  };
+});
+import { makeSubject } from 'wonka';
+import { createClient } from '@urql/core';
+import { useQuery } from './useQuery';
+import { nextTick, reactive } from 'vue';
+
+const client = createClient({ url: '/graphql', exchanges: [] });
+
+describe('useQuery', () => {
+  it('runs a query and updates data', async () => {
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => subject.source);
+
+    const _query = useQuery({
+      query: `{ test }`,
+    });
+    const query = reactive(_query);
+
+    expect(query).toMatchObject({
+      data: undefined,
+      stale: false,
+      fetching: true,
+      error: undefined,
+      extensions: undefined,
+      operation: undefined,
+      isPaused: false,
+      pause: expect.any(Function),
+      resume: expect.any(Function),
+      executeQuery: expect.any(Function),
+      then: expect.any(Function),
+    });
+
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+        context: undefined,
+      },
+      {
+        pollInterval: undefined,
+        requestPolicy: undefined,
+      }
+    );
+
+    expect(query.fetching).toBe(true);
+
+    subject.next({ data: { test: true } });
+
+    expect(query.fetching).toBe(false);
+    expect(query.data).toEqual({ test: true });
+  });
+
+  /*
+   * This test currently fails for the following reasons:
+   * with pause: false, `executeQuery` is called 3 times
+   * with pause: true, the promise from useQuery never resolves
+   *
+   * it's unclear to me what the desired behaviour is supposed to be.
+   * - If we want to have the query execute only once, we would have to set `pause: true`,
+   *   but that seems counter-intuitive
+   * - If we don't pause, I'd expect 2 calls:
+   *   -  one from watchEffect
+   *   -  one from useQuery().then()
+   *   I can't say why it's 3 in the latter case-
+   *
+   */
+  it.skip('runs Query and awaits results', async () => {
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => {
+        subject.next({ data: { test: true } });
+        return subject.source;
+      });
+
+    const query = await useQuery({
+      query: `{ test }`,
+      // pause: true,
+    });
+    expect(executeQuery).toHaveBeenCalledTimes(1);
+    expect(query.fetching.value).toBe(false);
+    expect(query.data.value).toEqual({ test: true });
+  });
+
+  it('pauses query when asked to do so', async () => {
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => subject.source);
+
+    const _query = useQuery({
+      query: `{ test }`,
+      pause: true,
+    });
+    const query = reactive(_query);
+
+    expect(executeQuery).not.toHaveBeenCalled();
+
+    query.resume();
+    await nextTick();
+    expect(query.fetching).toBe(true);
+
+    subject.next({ data: { test: true } });
+
+    expect(query.fetching).toBe(false);
+    expect(query.data).toEqual({ test: true });
+  });
+
+  it('updates when polling', async () => {
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeQuery')
+      .mockImplementation(() => subject.source);
+
+    const _query = useQuery({
+      query: `{ test }`,
+      pollInterval: 500,
+      requestPolicy: 'cache-first',
+    });
+    const query = reactive(_query);
+
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+        context: undefined,
+      },
+      {
+        pollInterval: 500,
+        requestPolicy: 'cache-first',
+      }
+    );
+
+    expect(query.fetching).toBe(true);
+
+    subject.next({ data: { test: true } });
+
+    expect(query.fetching).toBe(false);
+    expect(query.data).toEqual({ test: true });
+
+    subject.next({ data: { test: false } });
+    expect(query.data).toEqual({ test: false });
+  });
+});
diff --git a/packages/vue-urql/src/useSubscription.test.ts b/packages/vue-urql/src/useSubscription.test.ts
new file mode 100644
index 00000000..3db9b248
--- /dev/null
+++ b/packages/vue-urql/src/useSubscription.test.ts
@@ -0,0 +1,134 @@
+jest.mock('vue', () => {
+  const vue = jest.requireActual('vue');
+
+  return {
+    __esModule: true,
+    ...vue,
+    inject: () => client,
+  };
+});
+import { makeSubject } from 'wonka';
+import { createClient } from '@urql/core';
+import { useSubscription } from './useSubscription';
+import { nextTick, reactive, ref } from 'vue';
+
+const client = createClient({ url: '/graphql', exchanges: [] });
+
+describe('useSubscription', () => {
+  it('subscribes to a subscription and updates data', async () => {
+    const subject = makeSubject<any>();
+    const executeQuery = jest
+      .spyOn(client, 'executeSubscription')
+      .mockImplementation(() => subject.source);
+
+    const sub = reactive(
+      useSubscription({
+        query: `{ test }`,
+      })
+    );
+
+    expect(sub).toMatchObject({
+      data: undefined,
+      stale: false,
+      fetching: true,
+      error: undefined,
+      extensions: undefined,
+      operation: undefined,
+      isPaused: false,
+      pause: expect.any(Function),
+      resume: expect.any(Function),
+      executeSubscription: expect.any(Function),
+    });
+
+    expect(executeQuery).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      expect.any(Object)
+    );
+
+    expect(sub.fetching).toBe(true);
+
+    subject.next({ data: { test: true } });
+    expect(sub.data).toEqual({ test: true });
+    subject.complete();
+    expect(sub.fetching).toBe(false);
+  });
+
+  it('updates the executed subscription when inputs change', async () => {
+    const subject = makeSubject<any>();
+    const executeSubscription = jest
+      .spyOn(client, 'executeSubscription')
+      .mockImplementation(() => subject.source);
+
+    const variables = ref({});
+    const sub = reactive(
+      useSubscription({
+        query: `{ test }`,
+        variables,
+      })
+    );
+
+    expect(executeSubscription).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      expect.any(Object)
+    );
+
+    subject.next({ data: { test: true } });
+    expect(sub.data).toEqual({ test: true });
+
+    variables.value = { test: true };
+    await nextTick();
+    expect(executeSubscription).toHaveBeenCalledTimes(2);
+    expect(executeSubscription).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: { test: true },
+      },
+      expect.any(Object)
+    );
+
+    expect(sub.fetching).toBe(true);
+    expect(sub.data).toEqual({ test: true });
+  });
+  it('supports a custom scanning handler', async () => {
+    const subject = makeSubject<any>();
+    const executeSubscription = jest
+      .spyOn(client, 'executeSubscription')
+      .mockImplementation(() => subject.source);
+
+    const scanHandler = (currentState: any, nextState: any) => ({
+      counter: (currentState ? currentState.counter : 0) + nextState.counter,
+    });
+    const sub = reactive(
+      useSubscription(
+        {
+          query: `subscription { counter }`,
+        },
+        scanHandler
+      )
+    );
+
+    expect(executeSubscription).toHaveBeenCalledWith(
+      {
+        key: expect.any(Number),
+        query: expect.any(Object),
+        variables: {},
+      },
+      expect.any(Object)
+    );
+
+    subject.next({ data: { counter: 1 } });
+    expect(sub.data).toEqual({ counter: 1 });
+
+    subject.next({ data: { counter: 2 } });
+    expect(sub.data).toEqual({ counter: 3 });
+  });
+});