RD

rdc-vue-testing

Provides testing utilities for Data Client-enabled Vue applications.

Install

mkdir -p .claude/skills/rdc-vue-testing && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/5920" && unzip -o skill.zip -d .claude/skills/rdc-vue-testing && rm skill.zip

Installs to .claude/skills/rdc-vue-testing

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

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 on @data-client/vue.
320 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Test composables with renderDataCompose
  • Mount components with mountDataClient
  • Mock API responses
  • Test suspense behavior
  • Verify reactive prop changes

How it works

The skill provides testing utilities to mock store state, handle suspense, and verify reactive updates in Vue components using Data Client.

Inputs & outputs

You give it
Vue component or composable
You get back
Test suite results

When to use rdc-vue-testing

  • Test Vue composables
  • Mock API responses for testing
  • Test suspense behavior
  • Render components for unit testing

About this skill

Vue Testing Patterns (@data-client/vue)

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 responses
  • props - Reactive props object (use reactive())
  • 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 actions
  • wrapper - Vue Test Utils wrapper
  • cleanup() - Cleanup function (always call in afterEach/after test)
  • allSettled() - Wait for all pending promises
  • waitForNextUpdate() - 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() plus wrapper

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:

  1. jest.useFakeTimers() before mount/render (so the interval is created under fake timers).
  2. Render, then jest.advanceTimersByTime(frequency) to drive the initial fetch.
  3. Mutate the response (e.g. responseMock.mockReturnValue(...)), advance time again, await allSettled() and await nextTick().
  4. 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 renderData

Content truncated.

When not to use it

  • When testing non-Data Client components
  • When the project does not use Vue 3

Prerequisites

Vue Test Utils

Limitations

  • Requires Vue 3
  • Depends on Data Client hooks

How it compares

It integrates Data Client-specific lifecycle and state management into standard Vue testing patterns.

Compared to similar skills

rdc-vue-testing side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
rdc-vue-testing (this skill)13moReviewAdvanced
vue-testing-best-practices56moNo flagsIntermediate
vue06moNo flagsIntermediate
vue-best-practices194moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry