Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/api/__tests__/fc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, it } from "vitest";
import fc from "fast-check";

export const fcUndefinedOr = <T>(arb: fc.Arbitrary<T>) =>
fc.oneof(fc.constant(undefined), arb);

const fcError = fc.string().map((msg) => new Error(msg));

export const fcMockUseQueryResponseArg = <T>(arbData: fc.Arbitrary<T>) =>
fc.record({
data: fcUndefinedOr(arbData),
error: fcUndefinedOr(fcError),
});

export const fcMockUseSubscriptionResponseArg = <T>(arbData: fc.Arbitrary<T>) =>
fc.array(fcMockUseQueryResponseArg(arbData));

if (import.meta.vitest) {
it("fcUndefinedOr()", () => {
fc.assert(
fc.property(fcUndefinedOr(fc.integer()), (v) => {
expect(v === undefined || typeof v === "number").toBe(true);
}),
);
});

it("fcMockUseQueryResponse()", () => {
const fcData = fc.record({ a: fc.integer() });
const fcArg = fcMockUseQueryResponseArg(fcData);
fc.assert(
fc.property(fcArg, (v) => {
expect(v.data === undefined || typeof v.data.a === "number").toBe(true);
expect(v.error === undefined || v.error instanceof Error).toBe(true);
}),
);
});

it("fcMockUseSubscriptionResponse()", () => {
const fcData = fc.record({ a: fc.integer() });
const fcArg = fcMockUseSubscriptionResponseArg(fcData);
fc.assert(
fc.property(fcArg, (v) => {
expect(
v.every(
(e) =>
(e.data === undefined || typeof e.data.a === "number") &&
(e.error === undefined || e.error instanceof Error),
),
).toBe(true);
}),
);
});
}
59 changes: 59 additions & 0 deletions src/api/__tests__/run-property-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { vi, expect } from "vitest";
import type { ComputedRef } from "vue";
import { useQuery, useSubscription } from "@urql/vue";
import type { UseQueryResponse, UseSubscriptionResponse } from "@urql/vue";
import fc from "fast-check";

import { fcMockUseQueryResponseArg, fcMockUseSubscriptionResponseArg } from "./fc";
import { mockUseQueryResponse } from "./mock-use-query-response";
import { mockUseSubscriptionResponse } from "./mock-use-subscription-response";

interface _UseSubscribeXXXReturn<R> {
data: ComputedRef<R | undefined>;
error: ComputedRef<Error | undefined>;
}

type UseSubscribeXXXReturn<R> = _UseSubscribeXXXReturn<R> &
PromiseLike<_UseSubscribeXXXReturn<R>>;

export async function runPropertyTest<QueryData, SubData, R>(
useSubscribeXXX: () => UseSubscribeXXXReturn<R>,
mapQuery: (d: QueryData | undefined) => R | undefined,
mapSub: (d: SubData | undefined) => R | undefined,
fcQueryData: fc.Arbitrary<QueryData>,
fcSubData: fc.Arbitrary<SubData>,
) {
type QueryRes = UseQueryResponse<QueryData, any>;
type SubRes = UseSubscriptionResponse<SubData, SubData, any>;

const fcQueryArg = fcMockUseQueryResponseArg(fcQueryData);
const fcSubArg = fcMockUseSubscriptionResponseArg(fcSubData);

await fc.assert(
fc.asyncProperty(fcQueryArg, fcSubArg, async (queryArg, subArg) => {
// Mock useGenQuery()
const queryRes = mockUseQueryResponse<QueryData>(queryArg);
vi.mocked(useQuery<QueryData>).mockReturnValue(queryRes as QueryRes);

// Mock useGenSub()
const { response: subRes, issue } = mockUseSubscriptionResponse<SubData>(subArg);
vi.mocked(useSubscription<SubData>).mockReturnValue(subRes as SubRes);

const { data, error } = await useSubscribeXXX();

// Assert initial values are from query.
expect(error.value).toBe(queryArg.error);
expect(data.value).toBe(queryArg.error ? undefined : mapQuery(queryArg.data));

// Assert the subsequent values are issued from subscription backed up by query.
for (const issued of issue) {
const expectedError = issued.error || queryArg.error;
const expectedData = expectedError
? undefined
: (mapSub(issued.data) ?? mapQuery(queryArg.data));
expect(error.value).toBe(expectedError);
expect(data.value).toBe(expectedData);
}
}),
);
}
79 changes: 0 additions & 79 deletions src/api/__tests__/use-run-no-subscription.spec.ts

This file was deleted.

79 changes: 0 additions & 79 deletions src/api/__tests__/use-state-subscription.spec.ts

This file was deleted.

52 changes: 52 additions & 0 deletions src/api/__tests__/use-subscriptions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { it, vi } from "vitest";
import fc from "fast-check";

import type {
CtrlStateQuery,
CtrlStateSSubscription,
CtrlRunNoQuery,
CtrlRunNoSSubscription,
} from "@/graphql/codegen/generated";

import { useSubscribeRunNo } from "../use-run-no-subscription";
import { useSubscribeState } from "../use-state-subscription";

import { runPropertyTest } from "./run-property-test";

// Mock functions used in runPropertyTest()
vi.mock("@urql/vue", () => ({
useQuery: vi.fn(),
useSubscription: vi.fn(),
}));

it("useSubscribeState", async () => {
type QueryData = CtrlStateQuery;
type SubData = CtrlStateSSubscription;

const mapQuery = (d: QueryData | undefined) => d?.ctrl.state;
const mapSub = (d: SubData | undefined) => d?.ctrlState;

const fcCtrlState = fc.string();
const fcQueryData: fc.Arbitrary<QueryData> = fc.record({
ctrl: fc.record({ state: fcCtrlState }),
});
const fcSubData: fc.Arbitrary<SubData> = fc.record({ ctrlState: fcCtrlState });

await runPropertyTest(useSubscribeState, mapQuery, mapSub, fcQueryData, fcSubData);
});

it("useSubscribeRunNo", async () => {
type QueryData = CtrlRunNoQuery;
type SubData = CtrlRunNoSSubscription;

const mapQuery = (d: QueryData | undefined) => d?.ctrl.runNo;
const mapSub = (d: SubData | undefined) => d?.ctrlRunNo;

const fcCtrlRunNo = fc.integer();
const fcQueryData: fc.Arbitrary<QueryData> = fc.record({
ctrl: fc.record({ runNo: fcCtrlRunNo }),
});
const fcSubData: fc.Arbitrary<SubData> = fc.record({ ctrlRunNo: fcCtrlRunNo });

await runPropertyTest(useSubscribeRunNo, mapQuery, mapSub, fcQueryData, fcSubData);
});
31 changes: 31 additions & 0 deletions src/api/use-query-backed-subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ComputedRef, Ref } from "vue";
import { UseQueryResponse, UseSubscriptionResponse } from "@urql/vue";

import { onReady } from "@/utils/on-ready";
import type { OnReady } from "@/utils/on-ready";

import { useMappedWithFallback } from "./use-mapped-with-fallback";

interface UseQueryBackedSubscriptionOptions<T, Q, S> {
query: UseQueryResponse<Q>;
subscription: UseSubscriptionResponse<S>;
mapQueryData: (d: Ref<Q | undefined>) => T | undefined;
mapSubscriptionData: (d: Ref<S | undefined>) => T | undefined;
}

type UseQueryBackedSubscriptionReturn<T> = OnReady<{
data: ComputedRef<T | undefined>;
error: ComputedRef<Error | undefined>;
}>;

export function useQueryBackedSubscription<T, Q, S>(
options: UseQueryBackedSubscriptionOptions<T, Q, S>,
): UseQueryBackedSubscriptionReturn<T> {
const ret = useMappedWithFallback({
response1: options.subscription,
response2: options.query,
map1: options.mapSubscriptionData,
map2: options.mapQueryData,
});
return onReady(ret, options.query);
}
Loading
Loading