Vue Testing Patterns (@data-client/vue)
SkillDev toolsHelps your agent write and debug tests for Vue composables and components using fixtures and HTTP mocking.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Vue Testing Patterns (@data-client/vue) skill
About this capability
Test @data-client/vue composables and components - renderDataCompose, mountDataClient, fixtures, jest, nock HTTP mocking, polling/subscription tests with fake timers, useSuspense, useLive, useSubscription, Vue 3 reactive props. Use when writing or debugging tests for composables or components built
What this skill tells your AI
The instructions your AI receives, as published by reactive/data-client in .agents/skills/data-client-vue-testing/SKILL.md and read by ahel’s review.
Composable Testing with renderDataCompose()
import { renderDataCompose } from '../test';
import { reactive, computed } from 'vue';
it('useQuery() should return cached data', () => {
const { result } = renderDataCompose(
() => useQuery(Article, { id: 5 }),
{
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
},
],
},
);
expect(result.current?.value).toEqual(Article.fromJS({ id: 5, title: 'hi ho', content: 'whatever' }));
});
Options:
initialFixtures- Pre-populate store state (static fixtures)resolverFixtures- Intercept requests with dynamic responsesprops- Reactive props object (usereactive())managers,initialState,gcPolicy- Custom configuration
Return values:
result.current- Composable return value (undefined when suspended, Promise when resolved for useSuspense)controller- Controller instance for manual actionswrapper- Vue Test Utils wrappercleanup()- Cleanup function (always call in afterEach/after test)allSettled()- Wait for all pending promiseswaitForNextUpdate()- Wait for composable to resolve from suspended state
Component Testing with mountDataClient()
import { mountDataClient } from '../test';
import { defineComponent, h, reactive } from 'vue';
it('should render article component', async () => {
const ArticleComp = defineComponent({
props: { id: Number },
async setup(props) {
const article = await useSuspense(ArticleResource.get, { id: props.id });
return () => h('div', [
h('h3', article.value.title),
h('p', article.value.content),
]);
},
});
const props = reactive({ id: 5 });
const { wrapper, cleanup } = mountDataClient(ArticleComp, {
props,
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
},
],
});
await flushUntil(wrapper, () => wrapper.find('h3').exists());
expect(wrapper.find('h3').text()).toBe('hi ho');
cleanup();
});
Features:
- Suspense is automatically integrated (shows fallback while loading)
- Use
data-testid="suspense-fallback"to test loading state - Returns same utilities as
renderDataCompose()pluswrapper
Async Waiting Patterns
flushUntil helper (for component tests):
async function flushUntil(wrapper: any, predicate: () => boolean, tries = 100) {
for (let i = 0; i < tries; i++) {
if (predicate()) return;
await Promise.resolve();
await nextTick();
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Usage:
await flushUntil(wrapper, () => wrapper.find('h3').exists());
await flushUntil(wrapper, () => wrapper.find('h3').text() === 'Expected Title');
waitForNextUpdate (for composable tests):
const { result, waitForNextUpdate } = renderDataCompose(() => useSuspense(...));
// Initially suspended
expect(result.current).toBeUndefined();
// Wait for resolution
await waitForNextUpdate();
expect(result.current).toBeInstanceOf(Promise);
// Await the promise to get the reactive ComputedRef
const dataRef = await result.current;
expect(dataRef.value.title).toBe('hi ho');
Reactive Props Testing
Pattern 1: Testing prop changes:
const props = reactive({ id: 1 });
const { result } = renderDataCompose(
() => useQuery(Article, computed(() => ({ id: props.id }))),
{
initialFixtures: [
{ endpoint: ArticleResource.get, args: [{ id: 1 }], response: { id: 1, title: 'First' } },
{ endpoint: ArticleResource.get, args: [{ id: 2 }], response: { id: 2, title: 'Second' } },
],
},
);
expect(result.current?.value?.title).toBe('First');
// Change props - result automatically updates
props.id = 2;
expect(result.current?.value?.title).toBe('Second');
Pattern 2: Conditional arguments (null handling):
const props = reactive({ id: 1 as number | null });
const { result } = renderDataCompose(
(props: { id: number | null }) =>
useSuspense(ArticleResource.get, computed(() => props.id !== null ? { id: props.id } : null)),
{ props },
);
await waitForNextUpdate();
const articleRef = await result.current;
expect(articleRef.value).toBeDefined();
// Set to null - becomes undefined
props.id = null;
await nextTick();
expect(articleRef.value).toBeUndefined();
Fixtures and Interceptors
Static Fixture:
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'hi ho', content: 'whatever' },
}
Dynamic Interceptor:
resolverFixtures: [
{
endpoint: ArticleResource.get,
response: ({ id }) => ({ id, title: `Article ${id}`, content: 'dynamic' }),
},
]
Error Fixture:
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: new Error('Not found'),
error: true,
}
Testing Mutations
it('should update collection when pushed', async () => {
const { result, controller, waitForNextUpdate } = renderDataCompose(
() => useQuery(ArticleResource.getList.schema, {}),
{
initialFixtures: [
{ endpoint: ArticleResource.getList, args: [], response: [{ id: 1, title: 'First' }] },
],
resolverFixtures: [
{ endpoint: ArticleResource.getList.push, response: (body) => body },
],
},
);
expect(result.current?.value?.length).toBe(1);
await controller.fetch(ArticleResource.getList.push, {
id: 2,
title: 'Second',
content: 'new',
});
await waitForNextUpdate();
expect(result.current?.value?.length).toBe(2);
});
Testing with Controller
setResponse() for instant updates:
const { controller } = renderDataCompose(...);
await waitForNextUpdate();
const dataRef = await result.current;
expect(dataRef.value.title).toBe('Original');
controller.setResponse(
ArticleResource.get,
{ id: 5 },
{ id: 5, title: 'Updated', content: 'new content' }
);
await nextTick();
expect(dataRef.value.title).toBe('Updated'); // Reactive!
fetch() for mutations:
await controller.fetch(
ArticleResource.update,
{ id: 5 },
{ title: 'Mutated', content: 'mutated content' }
);
await nextTick();
Testing with nock (HTTP Mocking)
Use nock when a test must exercise the real fetch path — verifying URL construction, headers, request bodies, retries, or anything in your RestEndpoint/Resource networking layer. For pure store/state behavior, prefer initialFixtures/resolverFixtures (lighter and faster).
Minimal shape:
import nock from 'nock';
beforeAll(() => {
nock(/.*/)
.persist()
.defaultReplyHeaders({ 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' })
.options(/.*/).reply(200) // CORS preflight (required in JSDOM)
.get('/article/5').reply(200, { id: 5, title: 'hi ho' });
});
afterAll(() => nock.cleanAll());
For dynamic server state, mutating-closure replies, request spying with jest.fn(), error responses, and mixing nock with fixtures, see references/nock-http-mocking.md.
Testing Polling and Subscriptions
For composables with pollFrequency, useLive, or useSubscription, use fake timers so polls fire deterministically. Core flow:
jest.useFakeTimers()before mount/render (so the interval is created under fake timers).- Render, then
jest.advanceTimersByTime(frequency)to drive the initial fetch. - Mutate the response (e.g.
responseMock.mockReturnValue(...)), advance time again,await allSettled()andawait nextTick(). - Restore real timers in
afterEach:jest.useRealTimers().
Quick example:
jest.useFakeTimers();
const responseMock = jest.fn(() => payload);
const { result, allSettled, waitForNextUpdate, cleanup } = await renderDataCompose(
() => useSuspense(PollingArticleResource.get, { id: payload.id }),
{ resolverFixtures: [{ endpoint: PollingArticleResource.get, response: responseMock }] },
);
jest.advanceTimersByTime(frequency);
await allSettled();
await waitForNextUpdate();
const articleRef = await result;
responseMock.mockReturnValue({ ...payload, title: 'updated' });
jest.advanceTimersByTime(frequency);
await allSettled();
await nextTick();
expect(articleRef!.value.title).toBe('updated');
jest.useRealTimers();
cleanup();
For unsubscribe patterns, component-level polling tests, fake-timer-safe flushUntil, polling via nock, and common pitfalls, see references/polling-subscriptions.md.
Vue Suspense Behavior
useSuspense() returns Promise → ComputedRef:
const { result, waitForNextUpdate } = renderDataCompose(() =>
useSuspense(ArticleResource.get, { id: 5 })
);
// Initially suspended (undefined)
expect(result.current).toBeUndefined();
// Wait for resolution
await waitForNextUpdate();
// Now it's a Promise
expect(result.current).toBeInstanceOf(Promise);
// Await once to get reactive ComputedRef
const articleRef = await result.current;
// The ref is reactive - updates automatically
expect(articleRef.value.title).toBe('hi ho');
// After controller.setResponse() or controller.fetch():
await nextTick();
expect(articleRef.value.title).toBe('Updated'); // Auto-updated!
useQuery() returns ComputedRef directly:
const { result } = renderDataCompose(() => useQuery(Article, { id: 5 }));
// Synchronously available (or undefined if not in store)
expect(result.current?.value).toBeDefined();
expect(result.current?.value?.title).toBe('hi ho');
// Also reactive - updates automatically
Best Practices
- Always call cleanup() - Prevents memory leaks and test pollution
- Use renderDataCompose() for composables (useQuery, useSuspense, useLive)
- Use mountDataClient() for components
- Use reactive() for props - Enables testing prop changes
- Use computed() when passing reactive props to composables - Ensures proper reactivity tracking
- Use flushUntil() in component tests - More reliable than fixed delays
- Use waitForNextUpdate() in composable tests - Wait for suspension to resolve
- Remember nextTick() - After mutations/setResponse to allow Vue reactivity to propagate
- Use initialFixtures for initial state - Pre-populate the store
- Use resolverFixtures for dynamic responses - Intercept requests with functions
- useSuspense returns Promise → ComputedRef - Await once, then access
.value - Test both empty and populated states - Verify undefined behavior
- Test reactive prop changes - Use
reactive()and verify updates - Don't test with async setup + prop changes - Async setup only runs once; use non-async patterns or useFetch + watchEffect instead
References
For detailed API documentation, see the references directory:
- Fixtures - Fixture format reference
- unit-testing-hooks - Hook/composable testing guide
- nock-http-mocking - Full nock setup, dynamic server state, request spying, errors, pitfalls
- polling-subscriptions - Fake-timer patterns for
useLive/useSubscription/pollFrequency, unsubscribe verification, polling via nock
Common Patterns
Empty state test:
const { result } = renderDataCompose(() => useQuery(Article, { id: 5 }), {});
expect(result.current?.value).toBe(undefined);
Changing to non-existent entity:
const props = reactive({ id: 1 });
// ... initial setup ...
expect(result.current?.value?.id).toBe(1);
props.id = 999; // Not in store
expect(result.current?.value).toBe(undefined);
Testing nested collections:
const userTodos = new Collection(new schema.Array(Todo), {
argsKey: ({ userId }) => ({ userId }),
});
const { result } = renderDataCompose(
() => useQuery(userTodos, { userId: '1' }),
{ initialFixtures: [/* ... */] },
);
expect(result.current?.value?.length).toBe(2);
expect(result.current?.value?.[0]).toBeInstanceOf(Todo);
Signals
- GitHub stars
- 2k
- Forks
- 99
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
data-client-vue-testing- Source
- github.com/reactive/data-client