import { type Controller } from '@data-client/core';
import { type VueWrapper } from '@vue/test-utils';
import { defineComponent, h, watch, reactive, nextTick, isRef } from 'vue';
import {
mountDataClient,
type RenderDataClientOptions,
} from './mountDataClient.js';
/**
* Renders a Vue composable with DataClient provider for testing
*
* @see https://dataclient.io/docs/api/renderDataCompose
* @param composable - The composable function to test
* @param options - Configuration including optional reactive props ref, fixtures, managers, etc.
*/
export async function renderDataCompose
(
composable: (props: P) => R,
options: RenderDataClientOptions
= {},
): Promise<{
result: R;
wrapper: VueWrapper;
controller: Controller;
cleanup: () => void;
allSettled: () => Promise[]>;
waitForNextUpdate: () => Promise;
}> {
// Extract props from options, or create empty ref if not provided
const props = options.props || reactive({});
let resultRef: any;
let resolveNextUpdate: (() => void) | null = null;
let resolveComposableInit: (() => void) | null = null;
// Promise that resolves when the composable has been called
const composableInitialized = new Promise(resolve => {
resolveComposableInit = resolve;
});
const TestComponent = defineComponent({
name: 'TestComposableComponent',
props: {
composableProps: {
type: Object,
required: true,
},
},
setup(componentProps, { expose }) {
// Call the composable once with reactive props - Vue's useSuspense will handle the async behavior
const composableValue = composable(componentProps.composableProps as P);
// Store the composable value directly without wrapping
resultRef = composableValue;
// Signal that the composable has been initialized
if (resolveComposableInit) {
resolveComposableInit();
resolveComposableInit = null;
}
// If composable returns a Promise, wait for it to resolve before triggering update callbacks
if (composableValue instanceof Promise) {
composableValue
.then(() => {
if (resolveNextUpdate) {
resolveNextUpdate();
resolveNextUpdate = null;
}
})
.catch(() => {
if (resolveNextUpdate) {
resolveNextUpdate();
resolveNextUpdate = null;
}
});
} else {
// If composable returns a value directly, trigger update immediately
if (resolveNextUpdate) {
resolveNextUpdate();
resolveNextUpdate = null;
}
}
// Watch for changes to the result if it's reactive
if (isRef(resultRef)) {
watch(resultRef, () => {
if (resolveNextUpdate) {
resolveNextUpdate();
resolveNextUpdate = null;
}
});
}
expose({ result: resultRef });
return { result: resultRef };
},
render() {
return h('div', { 'data-testid': 'composable-result' });
},
});
// Wrap the component to accept composableProps from the external ref
const WrappedTestComponent = defineComponent({
name: 'WrappedTestComponent',
setup() {
return () =>
h(TestComponent, {
composableProps: props,
});
},
});
const { wrapper, controller, cleanup, allSettled } = mountDataClient(
WrappedTestComponent,
options,
);
// Wait for the composable to be initialized before returning
await composableInitialized;
// Give Vue's reactivity system time to process the initial values
await nextTick();
const waitForNextUpdate = async (): Promise => {
// For Promises (like useSuspense), wait for them to resolve
const valueToCheck = isRef(resultRef) ? resultRef.value : resultRef;
if (valueToCheck instanceof Promise) {
return new Promise(resolve => {
resolveNextUpdate = resolve;
setTimeout(() => {
if (resolveNextUpdate === resolve) {
resolveNextUpdate = null;
resolve();
}
}, 1000);
});
}
await allSettled();
await nextTick();
};
return {
result: resultRef,
wrapper,
controller,
cleanup,
allSettled,
waitForNextUpdate,
};
}