diff --git a/src/api/__tests__/mock-use-query-response.spec.ts b/src/api/__tests__/mock-use-query-response.spec.ts new file mode 100644 index 0000000..f617783 --- /dev/null +++ b/src/api/__tests__/mock-use-query-response.spec.ts @@ -0,0 +1,72 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useQuery } from "@urql/vue"; +import type { UseQueryResponse } from "@urql/vue"; +import gql from "graphql-tag"; +import fc from "fast-check"; + +import { mockUseQueryResponse } from "./mock-use-query-response"; + +vi.mock("@urql/vue", () => ({ + useQuery: vi.fn(), +})); + +const query = gql` + query CtrlState { + ctrl { + state + } + } +`; + +type Data = { ctrl: { state: string } }; + +const fcState = fc.string({ minLength: 1 }); +const fcData: fc.Arbitrary = fc.oneof( + fc.constant(undefined), + fc.record({ ctrl: fc.record({ state: fcState }) }), +); + +const fcErrorInstance = fc.string().map((msg) => new Error(msg)); +const fcError = fc.oneof(fc.constant(undefined), fcErrorInstance); + +const fcArg = fc.record({ data: fcData, error: fcError }); + +describe("mockUseQueryResponse()", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("Property test", async () => { + fc.assert( + fc.asyncProperty(fcArg, async (arg) => { + const response = mockUseQueryResponse(arg); + + type Response = UseQueryResponse; + vi.mocked(useQuery).mockReturnValue(response as Response); + + // Assert the mock response is returned. + const returned = useQuery({ query }); + expect(response).toBe(returned); + + // Assert initially undefined. + expect(response.error.value).toBeUndefined(); + expect(response.data.value).toBeUndefined(); + + // Await until the values are assigned. + const state = await response; + + // Confirm the object returned by await contains the same objects. + expect(state.data).toBe(response.data); + expect(state.error).toBe(response.error); + + // Assert the mocked values are assigned. + expect(response.error.value).toBe(arg.error); + expect(response.data.value).toStrictEqual(arg.data); + }), + ); + }); +}); diff --git a/src/api/__tests__/mock-use-query-response.ts b/src/api/__tests__/mock-use-query-response.ts new file mode 100644 index 0000000..398b959 --- /dev/null +++ b/src/api/__tests__/mock-use-query-response.ts @@ -0,0 +1,30 @@ +import { ref } from "vue"; +import type { Ref } from "vue"; + +import { onReady } from "@/utils/on-ready"; +import type { OnReady } from "@/utils/on-ready"; + +type MockUseQueryResponse = OnReady<{ + data: Ref; + error: Ref; +}>; + +interface MockUseQueryResponseArg { + data: T | undefined; + error: Error | undefined; +} + +export function mockUseQueryResponse( + arg: MockUseQueryResponseArg, +): MockUseQueryResponse { + const data = ref(undefined); + const error = ref(undefined); + + const ready = (async () => { + await Promise.resolve(); + data.value = arg.data; + error.value = arg.error; + })(); + + return onReady({ data, error }, ready) as MockUseQueryResponse; +} diff --git a/src/api/__tests__/mock-use-subscription-response.spec.ts b/src/api/__tests__/mock-use-subscription-response.spec.ts new file mode 100644 index 0000000..f8d694b --- /dev/null +++ b/src/api/__tests__/mock-use-subscription-response.spec.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useSubscription } from "@urql/vue"; +import type { UseSubscriptionResponse } from "@urql/vue"; +import gql from "graphql-tag"; +import fc from "fast-check"; + +import { mockUseSubscriptionResponse } from "./mock-use-subscription-response"; + +vi.mock("@urql/vue", () => ({ + useSubscription: vi.fn(), +})); + +export const query = gql` + subscription CtrlStateS { + ctrlState + } +`; + +export type Data = { ctrlState: string }; + +const fcState = fc.string({ minLength: 1 }); +const fcData = fc.oneof(fc.constant(undefined), fc.record({ ctrlState: fcState })); + +const fcErrorInstance = fc.string().map((msg) => new Error(msg)); +const fcError = fc.oneof(fc.constant(undefined), fcErrorInstance); + +const fcArg = fc.record({ data: fcData, error: fcError }); + +describe("mockUseSubscriptionResponse()", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + it("Property test", () => { + fc.assert( + fc.property(fc.array(fcArg), (arg) => { + const { response, issue } = mockUseSubscriptionResponse(arg); + + type Response = UseSubscriptionResponse; + vi.mocked(useSubscription).mockReturnValue(response as Response); + + // Assert the mock response is returned. + const returned = useSubscription({ query }); + expect(returned).toBe(response); + + // Assert the mocked values are issued to the subscription. + for (const issued of issue) { + expect(response.error.value).toBe(issued.error); + expect(response.data.value).toStrictEqual(issued.data); + } + }), + ); + }); +}); diff --git a/src/api/__tests__/mock-use-subscription-response.ts b/src/api/__tests__/mock-use-subscription-response.ts new file mode 100644 index 0000000..eaa6b3e --- /dev/null +++ b/src/api/__tests__/mock-use-subscription-response.ts @@ -0,0 +1,38 @@ +import { ref } from "vue"; +import type { Ref } from "vue"; + +interface MockUseSubscriptionResponseArgElement { + data: T | undefined; + error: Error | undefined; +} + +type MockUseSubscriptionResponseArg = Iterable< + MockUseSubscriptionResponseArgElement +>; + +interface MockUseSubscriptionResponse { + response: { + data: Ref; + error: Ref; + }; + issue: MockUseSubscriptionResponseArg; +} + +export function mockUseSubscriptionResponse( + arg: MockUseSubscriptionResponseArg, +): MockUseSubscriptionResponse { + const data = ref(undefined); + const error = ref(undefined); + + function* _issue() { + for (const res of arg) { + data.value = res.data; + error.value = res.error; + yield res; + } + } + const issue = _issue(); + const response = { data, error }; + + return { response, issue } as MockUseSubscriptionResponse; +} diff --git a/src/api/__tests__/use-state-subscription.spec.ts b/src/api/__tests__/use-state-subscription.spec.ts index 071bc32..a074146 100644 --- a/src/api/__tests__/use-state-subscription.spec.ts +++ b/src/api/__tests__/use-state-subscription.spec.ts @@ -1,61 +1,37 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ref } from "vue"; import fc from "fast-check"; +import type { + CtrlStateQuery, + CtrlStateSSubscription, +} from "@/graphql/codegen/generated"; import { useCtrlStateQuery, useCtrlStateSSubscription, } from "@/graphql/codegen/generated"; -import type { CtrlStateSSubscription } from "@/graphql/codegen/generated"; -import { onReady } from "@/utils/on-ready"; import { useSubscribeState } from "../use-state-subscription"; +import { mockUseQueryResponse } from "./mock-use-query-response"; +import { mockUseSubscriptionResponse } from "./mock-use-subscription-response"; + vi.mock("@/graphql/codegen/generated", () => ({ useCtrlStateQuery: vi.fn(), useCtrlStateSSubscription: vi.fn(), })); -const fcState = fc.oneof(fc.constant(undefined), fc.string({ minLength: 1 })); +const fcState = fc.string({ minLength: 1 }); +const fcQueryData = fc.oneof( + fc.constant(undefined), + fc.record({ ctrl: fc.record({ state: fcState }) }), +); +const fcSubData = fc.oneof(fc.constant(undefined), fc.record({ ctrlState: fcState })); const fcErrorInstance = fc.string().map((msg) => new Error(msg)); const fcError = fc.oneof(fc.constant(undefined), fcErrorInstance); -type Query = ReturnType; -type Sub = ReturnType; - -function createMockQuery( - state_value: string | undefined, - error_value: Error | undefined, -): Query { - type Data = NonNullable; - const data = ref(undefined); - const error = ref(undefined); - - const ready = (async () => { - await Promise.resolve(); - data.value = { ctrl: { state: state_value } } as Data; - error.value = error_value; - })(); - - return onReady({ data, error }, ready) as Query; -} - -function createMockSubscription( - state_value: string | undefined, - error_value: Error | undefined, -): Sub { - const data = ref(undefined); - const error = ref(undefined); - - const ready = (async () => { - await Promise.resolve(); - data.value = { ctrlState: state_value } as CtrlStateSSubscription; - error.value = error_value; - })(); - - return onReady({ data, error }, ready) as unknown as Sub; -} +const fcQueryArg = fc.record({ data: fcQueryData, error: fcError }); +const fcSubArg = fc.record({ data: fcSubData, error: fcError }); describe("useSubscribeState()", () => { beforeEach(() => { @@ -68,26 +44,36 @@ describe("useSubscribeState()", () => { it("Property test", async () => { await fc.assert( - fc.asyncProperty( - fcState, - fcError, - fcState, - fcError, - async (queryState, queryError, subState, subError) => { - const query = createMockQuery(queryState, queryError); - const sub = createMockSubscription(subState, subError); - vi.mocked(useCtrlStateQuery).mockReturnValue(query); - vi.mocked(useCtrlStateSSubscription).mockReturnValue(sub); - const { state, error } = await useSubscribeState(); - const expectedError = subError || queryError; + fc.asyncProperty(fcQueryArg, fc.array(fcSubArg), async (queryArg, subArg) => { + // Mock useCtrlStateQuery() + const queryRes = mockUseQueryResponse(queryArg); + type Query = ReturnType; + vi.mocked(useCtrlStateQuery).mockReturnValue(queryRes as Query); + + // Mock useCtrlStateSSubscription() + const { response: subRes, issue } = + mockUseSubscriptionResponse(subArg); + type SubRes = ReturnType; + vi.mocked(useCtrlStateSSubscription).mockReturnValue(subRes as SubRes); + + const { state, error } = await useSubscribeState(); + + // Assert initial values are from query. + expect(error.value).toBe(queryArg.error); + expect(state.value).toBe( + queryArg.error ? undefined : queryArg.data?.ctrl.state, + ); + + // Assert the subsequent values are issued from subscription backed up by query. + for (const issued of issue) { + const expectedError = issued.error || queryArg.error; const expectedState = expectedError ? undefined - : subState || queryState || undefined; - + : issued.data?.ctrlState || queryArg.data?.ctrl.state; expect(error.value).toBe(expectedError); expect(state.value).toBe(expectedState); - }, - ), + } + }), ); }); });