Skip to content
Merged
72 changes: 72 additions & 0 deletions src/api/__tests__/mock-use-query-response.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Data | undefined> = 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<Data>(arg);

type Response = UseQueryResponse<Data>;
vi.mocked(useQuery).mockReturnValue(response as Response);

// Assert the mock response is returned.
const returned = useQuery<Data>({ 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);
}),
);
});
});
30 changes: 30 additions & 0 deletions src/api/__tests__/mock-use-query-response.ts
Original file line number Diff line number Diff line change
@@ -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<T> = OnReady<{
data: Ref<T | undefined>;
error: Ref<Error | undefined>;
}>;

interface MockUseQueryResponseArg<T> {
data: T | undefined;
error: Error | undefined;
}

export function mockUseQueryResponse<T>(
arg: MockUseQueryResponseArg<T>,
): MockUseQueryResponse<T> {
const data = ref<T | undefined>(undefined);
const error = ref<Error | undefined>(undefined);

const ready = (async () => {
await Promise.resolve();
data.value = arg.data;
error.value = arg.error;
})();

return onReady({ data, error }, ready) as MockUseQueryResponse<T>;
}
58 changes: 58 additions & 0 deletions src/api/__tests__/mock-use-subscription-response.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Data>(arg);

type Response = UseSubscriptionResponse<Data>;
vi.mocked(useSubscription).mockReturnValue(response as Response);

// Assert the mock response is returned.
const returned = useSubscription<Data>({ 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);
}
}),
);
});
});
38 changes: 38 additions & 0 deletions src/api/__tests__/mock-use-subscription-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ref } from "vue";
import type { Ref } from "vue";

interface MockUseSubscriptionResponseArgElement<T> {
data: T | undefined;
error: Error | undefined;
}

type MockUseSubscriptionResponseArg<T> = Iterable<
MockUseSubscriptionResponseArgElement<T>
>;

interface MockUseSubscriptionResponse<T> {
response: {
data: Ref<T | undefined>;
error: Ref<Error | undefined>;
};
issue: MockUseSubscriptionResponseArg<T>;
}

export function mockUseSubscriptionResponse<T>(
arg: MockUseSubscriptionResponseArg<T>,
): MockUseSubscriptionResponse<T> {
const data = ref<T | undefined>(undefined);
const error = ref<Error | undefined>(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<T>;
}
96 changes: 41 additions & 55 deletions src/api/__tests__/use-state-subscription.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof useCtrlStateQuery>;
type Sub = ReturnType<typeof useCtrlStateSSubscription>;

function createMockQuery(
state_value: string | undefined,
error_value: Error | undefined,
): Query {
type Data = NonNullable<Query["data"]["value"]>;
const data = ref<Data | undefined>(undefined);
const error = ref<Error | undefined>(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<CtrlStateSSubscription | undefined>(undefined);
const error = ref<Error | undefined>(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(() => {
Expand All @@ -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<CtrlStateQuery>(queryArg);
type Query = ReturnType<typeof useCtrlStateQuery>;
vi.mocked(useCtrlStateQuery).mockReturnValue(queryRes as Query);

// Mock useCtrlStateSSubscription()
const { response: subRes, issue } =
mockUseSubscriptionResponse<CtrlStateSSubscription>(subArg);
type SubRes = ReturnType<typeof useCtrlStateSSubscription>;
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);
},
),
}
}),
);
});
});
Loading