Skip to content

Commit d877c59

Browse files
authored
Merge pull request #85 from simonsobs/dev
Add example tests
2 parents 8c1986a + 855ea18 commit d877c59

File tree

9 files changed

+503
-1
lines changed

9 files changed

+503
-1
lines changed

src/api/__tests__/app-disposer.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { App } from "vue";
2+
3+
/**
4+
* A class that wraps an application and implements the Disposable interface.
5+
* It provides a way to properly unmount the app when the instance is disposed.
6+
*
7+
* This class is designed to be used with the `using` statement, which will
8+
* automatically unmount the app when the block exits.
9+
*
10+
* @example
11+
* ```
12+
* {
13+
* using disposer = new AppDisposer(app);
14+
* // Use the app...
15+
* } // The app is automatically unmounted here
16+
* ```
17+
*/
18+
export class AppDisposer implements Disposable {
19+
/**
20+
* Creates a new AppDisposer instance.
21+
* @param app - The application instance to be managed.
22+
*/
23+
constructor(private app: App) {}
24+
25+
/**
26+
* Implements the dispose method of the Disposable interface.
27+
* This method is called automatically when the `using` block exits,
28+
* ensuring that the app is properly unmounted.
29+
*/
30+
[Symbol.dispose]() {
31+
this.app.unmount();
32+
}
33+
}
34+
35+
if (import.meta.vitest) {
36+
const { it, expect, vi } = import.meta.vitest;
37+
38+
it("using", () => {
39+
const mockApp = {
40+
unmount: vi.fn(),
41+
};
42+
43+
{
44+
using _ = new AppDisposer(mockApp as unknown as App);
45+
expect(mockApp.unmount).not.toHaveBeenCalled();
46+
}
47+
48+
expect(mockApp.unmount).toHaveBeenCalledTimes(1);
49+
});
50+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Client, CombinedError, provideClient, useQuery } from "@urql/vue";
2+
import gql from "graphql-tag";
3+
import { describe, expect, it } from "vitest";
4+
import type { App, MaybeRef } from "vue";
5+
import { createApp, defineComponent, ref } from "vue";
6+
import { fromValue, never } from "wonka";
7+
8+
type SetupFunction = () => Record<string, unknown>;
9+
10+
interface WitSetupsOptions {
11+
setupParent: SetupFunction;
12+
setupChild: SetupFunction;
13+
}
14+
15+
class DisposableApp implements Disposable {
16+
constructor(private app: App) {}
17+
18+
[Symbol.dispose]() {
19+
this.app.unmount();
20+
}
21+
}
22+
23+
function withSetups(options: WitSetupsOptions) {
24+
const ParentComponent = defineComponent({
25+
setup: options.setupParent,
26+
template: "<child-component />",
27+
});
28+
29+
const ChildComponent = defineComponent({
30+
setup: options.setupChild,
31+
template: "<template />",
32+
});
33+
34+
const app = createApp(ParentComponent);
35+
app.component("ChildComponent", ChildComponent);
36+
app.mount(document.createElement("div"));
37+
const disposable = new DisposableApp(app);
38+
return { app, [Symbol.dispose]: () => disposable[Symbol.dispose]() };
39+
}
40+
41+
export const FooDocument = gql`
42+
query foo {
43+
foo
44+
}
45+
`;
46+
47+
export type FooQuery = {
48+
__typename?: "Query";
49+
foo?: string | null;
50+
};
51+
52+
function useQueryFromClient(client: MaybeRef<Client>) {
53+
let query: ReturnType<typeof useQuery<FooQuery>> | undefined;
54+
const app = withSetups({
55+
setupParent() {
56+
provideClient(client);
57+
return {};
58+
},
59+
setupChild() {
60+
query = useQuery<FooQuery>({ query: FooDocument });
61+
return {};
62+
},
63+
});
64+
if (!query) throw new Error("query is undefined");
65+
return { ...app, query, [Symbol.dispose]: () => app[Symbol.dispose]() };
66+
}
67+
68+
describe("one", () => {
69+
it("fetching", () => {
70+
const executeQuery = () => never; // fetching
71+
const client = ref({ executeQuery });
72+
using res = useQueryFromClient(client as any);
73+
const { query } = res;
74+
expect(query.fetching.value).toBe(true);
75+
});
76+
77+
it("data", () => {
78+
const executeQuery = () => fromValue({ data: { foo: "bar" } });
79+
const client = ref({ executeQuery });
80+
using res = useQueryFromClient(client as any);
81+
const { query } = res;
82+
expect(query.fetching.value).toBe(false);
83+
expect(query.data.value?.foo).toBe("bar");
84+
});
85+
86+
it("error", () => {
87+
const executeQuery = () =>
88+
fromValue({
89+
error: new CombinedError({ networkError: new Error("baz") }),
90+
});
91+
const client = ref({ executeQuery });
92+
using res = useQueryFromClient(client as any);
93+
const { query } = res;
94+
expect(query.error.value?.message).toEqual(expect.stringContaining("baz"));
95+
});
96+
97+
it("example - reactive", async () => {
98+
const executeQuery = () => never; // fetching
99+
const client = ref({ executeQuery });
100+
using res = useQueryFromClient(client as any);
101+
const { query } = res;
102+
103+
// fetching initially
104+
expect(query.fetching.value).toBe(true);
105+
106+
// provide data
107+
client.value.executeQuery = () => fromValue({ data: { foo: "bar" } });
108+
109+
await query;
110+
111+
// not fetching anymore, data is available
112+
expect(query.fetching.value).toBe(false);
113+
expect(query.data.value?.foo).toBe("bar");
114+
});
115+
});

src/api/__tests__/scratch.spec.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Client, CombinedError } from "@urql/vue";
2+
import { describe, expect, it, vi } from "vitest";
3+
import type { MaybeRef } from "vue";
4+
import { ref } from "vue";
5+
import type { Subject } from "wonka";
6+
import { fromValue, makeSubject, never } from "wonka";
7+
8+
import type { CtrlStateQuery } from "@/graphql/codegen/generated";
9+
import { useCtrlStateQuery } from "@/graphql/codegen/generated";
10+
11+
import { useInSetupWithUrqlClient } from "./setup-with-client";
12+
13+
function useQueryFromClient(client: MaybeRef<Client>) {
14+
const disposable = useInSetupWithUrqlClient(
15+
() => useCtrlStateQuery({ variables: {} }),
16+
client
17+
);
18+
const { ret: query } = disposable;
19+
return { query, [Symbol.dispose]: () => disposable[Symbol.dispose]() };
20+
}
21+
22+
describe("useCtrlStateQuery with mock client", () => {
23+
it("executeQuery is called", () => {
24+
const executeQuery = vi.fn();
25+
const client = ref({ executeQuery });
26+
using _ = useQueryFromClient(client as any);
27+
expect(executeQuery).toBeCalledTimes(1);
28+
});
29+
30+
it("fetching", () => {
31+
const executeQuery = vi.fn(() => never);
32+
const client = ref({ executeQuery });
33+
using res = useQueryFromClient(client as any);
34+
const { query } = res;
35+
expect(executeQuery).toBeCalledTimes(1);
36+
expect(query.fetching.value).toBe(true);
37+
});
38+
39+
it("data", async () => {
40+
const data: CtrlStateQuery = { ctrl: { state: "initialized" } };
41+
const executeQuery = vi.fn(() => fromValue({ data }));
42+
const client = ref({ executeQuery });
43+
using res = useQueryFromClient(client as any);
44+
const { query } = res;
45+
expect(executeQuery).toBeCalledTimes(1);
46+
expect(query.data.value).toEqual(data);
47+
});
48+
49+
it("error", async () => {
50+
const error = new CombinedError({ networkError: Error("something went wrong!") });
51+
const executeQuery = vi.fn(() => fromValue({ error }));
52+
const client = ref({ executeQuery });
53+
using res = useQueryFromClient(client as any);
54+
const { query } = res;
55+
expect(executeQuery).toBeCalledTimes(1);
56+
expect(query.error.value).toEqual(error);
57+
});
58+
59+
it("data after fetching", async () => {
60+
const data: CtrlStateQuery = { ctrl: { state: "initialized" } };
61+
const subject: Subject<{ data?: CtrlStateQuery; error?: Error }> = makeSubject();
62+
const executeQuery = vi.fn(() => subject.source);
63+
const client = ref({ executeQuery });
64+
65+
using res = useQueryFromClient(client as any);
66+
const { query } = res;
67+
68+
expect(executeQuery).toBeCalledTimes(1);
69+
expect(query.fetching.value).toBe(true);
70+
71+
setTimeout(() => {
72+
subject.next({ data });
73+
}, 0);
74+
75+
await query;
76+
77+
expect(executeQuery).toBeCalledTimes(1);
78+
expect(query.fetching.value).toBe(false);
79+
expect(query.data.value).toEqual(data);
80+
});
81+
82+
it("error after fetching", async () => {
83+
const error = new CombinedError({ networkError: Error("something went wrong!") });
84+
const subject: Subject<{ data?: CtrlStateQuery; error?: Error }> = makeSubject();
85+
const executeQuery = vi.fn(() => subject.source);
86+
const client = ref({ executeQuery });
87+
88+
using res = useQueryFromClient(client as any);
89+
const { query } = res;
90+
91+
expect(executeQuery).toBeCalledTimes(1);
92+
expect(query.fetching.value).toBe(true);
93+
94+
setTimeout(() => {
95+
subject.next({ error });
96+
}, 0);
97+
98+
await query;
99+
100+
expect(executeQuery).toBeCalledTimes(1);
101+
expect(query.fetching.value).toBe(false);
102+
expect(query.error.value).toEqual(error);
103+
});
104+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createApp, defineComponent, inject, provide } from "vue";
2+
3+
import { AppDisposer } from "./app-disposer";
4+
5+
type SetupFunction = () => Record<string, unknown>;
6+
7+
/**
8+
* Options for setting up nested components.
9+
*/
10+
export interface SetupNestedComponentsOptions {
11+
/** Setup function for the parent component */
12+
setupParent: SetupFunction;
13+
/** Setup function for the child component */
14+
setupChild: SetupFunction;
15+
}
16+
17+
/**
18+
* Sets up a Vue application with nested parent and child components.
19+
*
20+
* This function creates a Vue application with a parent component that includes
21+
* a child component. Both components are configured using the provided setup functions.
22+
* The resulting app is mounted to a new div element and wrapped in an AppDisposer
23+
* for proper cleanup.
24+
*
25+
* @param options - Configuration options for the parent and child components
26+
* @returns An object containing the Vue app instance and a Symbol.dispose method
27+
*
28+
* @example
29+
* ```
30+
* const { app } = setupNestedComponents({
31+
* setupParent: () => ({ parentData: ref('Parent') }),
32+
* setupChild: () => ({ childData: ref('Child') })
33+
* });
34+
* ```
35+
*/
36+
export function setupNestedComponents(options: SetupNestedComponentsOptions) {
37+
const ParentComponent = defineComponent({
38+
setup: options.setupParent,
39+
template: "<child-component />",
40+
});
41+
42+
const ChildComponent = defineComponent({
43+
setup: options.setupChild,
44+
template: "<template />",
45+
});
46+
47+
const app = createApp(ParentComponent);
48+
app.component("ChildComponent", ChildComponent);
49+
app.mount(document.createElement("div"));
50+
const disposable = new AppDisposer(app);
51+
return { app, [Symbol.dispose]: () => disposable[Symbol.dispose]() };
52+
}
53+
54+
if (import.meta.vitest) {
55+
const { it, expect } = import.meta.vitest;
56+
57+
it("setupNestedComponents", () => {
58+
const key = Symbol();
59+
const provided = {}; // a sentinel
60+
let injected: object | undefined;
61+
62+
using _ = setupNestedComponents({
63+
setupParent: () => {
64+
provide(key, provided);
65+
return {};
66+
},
67+
setupChild: () => {
68+
injected = inject(key);
69+
return {};
70+
},
71+
});
72+
73+
expect(injected).toBe(provided);
74+
});
75+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Client } from "@urql/vue";
2+
import { provideClient, useClientHandle } from "@urql/vue";
3+
import type { MaybeRef } from "vue";
4+
5+
import { setupNestedComponents } from "./setup-nested-components";
6+
7+
8+
/**
9+
* Execute `func` in a Vue component `setup` in which the `client` can be injected.
10+
*
11+
* @param func - Function to be executed
12+
* @param client - The urql client to be provided
13+
* @returns A disposable object containing the `func`'s return
14+
*/
15+
export function useInSetupWithUrqlClient<T>(func: () => T, client: MaybeRef<Client>) {
16+
let ret!: T;
17+
const disposable = setupNestedComponents({
18+
setupParent() {
19+
provideClient(client);
20+
return {};
21+
},
22+
setupChild() {
23+
ret = func();
24+
return {};
25+
},
26+
});
27+
return { ret, [Symbol.dispose]: () => disposable[Symbol.dispose]() };
28+
}
29+
30+
if (import.meta.vitest) {
31+
const { it, expect } = import.meta.vitest;
32+
33+
it("useInSetupWithUrqlClient", () => {
34+
const mockClient = {} as Client;
35+
36+
using ret = useInSetupWithUrqlClient(() => useClientHandle(), mockClient);
37+
const { ret: handle } = ret;
38+
39+
expect(handle.client).toBe(mockClient);
40+
});
41+
}

0 commit comments

Comments
 (0)