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
101 changes: 90 additions & 11 deletions src/hooks/__tests__/useViewModelInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function createMockViewModel(options?: {
function createMockRiveFile(options?: {
defaultViewModel?: ViewModel;
artboardViewModels?: Record<string, ViewModel>;
namedViewModels?: Record<string, ViewModel>;
}): RiveFile {
return {
dispose: jest.fn(),
Expand All @@ -51,7 +52,9 @@ function createMockRiveFile(options?: {
artboardCount: 0,
artboardNames: [],
viewModelByIndex: jest.fn(),
viewModelByName: jest.fn(),
viewModelByName: jest.fn(
(name: string) => options?.namedViewModels?.[name]
),
defaultArtboardViewModel: jest.fn((artboardBy?: ArtboardBy) => {
if (artboardBy?.name && options?.artboardViewModels) {
return options.artboardViewModels[artboardBy.name];
Expand All @@ -62,8 +65,8 @@ function createMockRiveFile(options?: {
} as any;
}

describe('useViewModelInstance - RiveFile with name parameter', () => {
it('should use createInstanceByName when name is provided with RiveFile', () => {
describe('useViewModelInstance - RiveFile with instanceName parameter', () => {
it('should use createInstanceByName when instanceName is provided with RiveFile', () => {
const personInstance = createMockViewModelInstance();
const defaultViewModel = createMockViewModel({
namedInstances: { PersonInstance: personInstance },
Expand All @@ -72,7 +75,7 @@ describe('useViewModelInstance - RiveFile with name parameter', () => {
const mockRiveFile = createMockRiveFile({ defaultViewModel });

const { result } = renderHook(() =>
useViewModelInstance(mockRiveFile, { name: 'PersonInstance' })
useViewModelInstance(mockRiveFile, { instanceName: 'PersonInstance' })
);

expect(mockRiveFile.defaultArtboardViewModel).toHaveBeenCalledWith(
Expand All @@ -85,7 +88,7 @@ describe('useViewModelInstance - RiveFile with name parameter', () => {
expect(result.current).toBe(personInstance);
});

it('should use defaultArtboardViewModel and createDefaultInstance when no name provided', () => {
it('should use defaultArtboardViewModel and createDefaultInstance when no instanceName provided', () => {
const defaultInstance = createMockViewModelInstance();
const defaultViewModel = createMockViewModel({ defaultInstance });

Expand All @@ -109,7 +112,7 @@ describe('useViewModelInstance - RiveFile with name parameter', () => {
const mockRiveFile = createMockRiveFile({ defaultViewModel });

const { result } = renderHook(() =>
useViewModelInstance(mockRiveFile, { name: 'NonExistent' })
useViewModelInstance(mockRiveFile, { instanceName: 'NonExistent' })
);

expect(result.current).toBeNull();
Expand All @@ -125,7 +128,7 @@ describe('useViewModelInstance - RiveFile with name parameter', () => {
expect(() =>
renderHook(() =>
useViewModelInstance(mockRiveFile, {
name: 'NonExistent',
instanceName: 'NonExistent',
required: true,
})
)
Expand Down Expand Up @@ -159,7 +162,7 @@ describe('useViewModelInstance - RiveFile with name parameter', () => {
).toThrow("Artboard 'MissingArtboard' not found or has no ViewModel");
});

it('should call onInit when instance is created with name parameter', () => {
it('should call onInit when instance is created with instanceName parameter', () => {
const personInstance = createMockViewModelInstance();
const defaultViewModel = createMockViewModel({
namedInstances: { PersonInstance: personInstance },
Expand All @@ -169,7 +172,10 @@ describe('useViewModelInstance - RiveFile with name parameter', () => {
const mockRiveFile = createMockRiveFile({ defaultViewModel });

renderHook(() =>
useViewModelInstance(mockRiveFile, { name: 'PersonInstance', onInit })
useViewModelInstance(mockRiveFile, {
instanceName: 'PersonInstance',
onInit,
})
);

expect(onInit).toHaveBeenCalledWith(personInstance);
Expand Down Expand Up @@ -199,7 +205,7 @@ describe('useViewModelInstance - RiveFile with artboardName parameter', () => {
expect(result.current).toBe(mainInstance);
});

it('should combine artboardName and name to get specific instance from specific artboard', () => {
it('should combine artboardName and instanceName to get specific instance from specific artboard', () => {
const specificInstance = createMockViewModelInstance();
const mainArtboardViewModel = createMockViewModel({
namedInstances: { SpecificInstance: specificInstance },
Expand All @@ -212,7 +218,7 @@ describe('useViewModelInstance - RiveFile with artboardName parameter', () => {
const { result } = renderHook(() =>
useViewModelInstance(mockRiveFile, {
artboardName: 'MainArtboard',
name: 'SpecificInstance',
instanceName: 'SpecificInstance',
})
);

Expand All @@ -227,6 +233,79 @@ describe('useViewModelInstance - RiveFile with artboardName parameter', () => {
});
});

describe('useViewModelInstance - RiveFile with viewModelName parameter', () => {
it('should use viewModelByName when viewModelName is provided', () => {
const settingsInstance = createMockViewModelInstance();
const settingsViewModel = createMockViewModel({
defaultInstance: settingsInstance,
});

const mockRiveFile = createMockRiveFile({
namedViewModels: { Settings: settingsViewModel },
});

const { result } = renderHook(() =>
useViewModelInstance(mockRiveFile, { viewModelName: 'Settings' })
);

expect(mockRiveFile.viewModelByName).toHaveBeenCalledWith('Settings');
expect(mockRiveFile.defaultArtboardViewModel).not.toHaveBeenCalled();
expect(settingsViewModel.createDefaultInstance).toHaveBeenCalled();
expect(result.current).toBe(settingsInstance);
});

it('should return null when viewModelName not found and required is false', () => {
const mockRiveFile = createMockRiveFile({
namedViewModels: {},
});

const { result } = renderHook(() =>
useViewModelInstance(mockRiveFile, { viewModelName: 'NonExistent' })
);

expect(result.current).toBeNull();
});

it('should throw when viewModelName not found and required is true', () => {
const mockRiveFile = createMockRiveFile({
namedViewModels: {},
});

expect(() =>
renderHook(() =>
useViewModelInstance(mockRiveFile, {
viewModelName: 'NonExistent',
required: true,
})
)
).toThrow("ViewModel 'NonExistent' not found");
});

it('should combine viewModelName and instanceName to get specific instance', () => {
const specificInstance = createMockViewModelInstance();
const settingsViewModel = createMockViewModel({
namedInstances: { UserSettings: specificInstance },
});

const mockRiveFile = createMockRiveFile({
namedViewModels: { Settings: settingsViewModel },
});

const { result } = renderHook(() =>
useViewModelInstance(mockRiveFile, {
viewModelName: 'Settings',
instanceName: 'UserSettings',
})
);

expect(mockRiveFile.viewModelByName).toHaveBeenCalledWith('Settings');
expect(settingsViewModel.createInstanceByName).toHaveBeenCalledWith(
'UserSettings'
);
expect(result.current).toBe(specificInstance);
});
});

describe('useViewModelInstance - ViewModel source', () => {
it('should use createInstanceByName when name is provided with ViewModel', () => {
const namedInstance = createMockViewModelInstance();
Expand Down
118 changes: 92 additions & 26 deletions src/hooks/useViewModelInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,55 @@ interface UseViewModelInstanceBaseParams {
onInit?: (instance: ViewModelInstance) => void;
}

export interface UseViewModelInstanceFileParams
interface UseViewModelInstanceFileBaseParams
extends UseViewModelInstanceBaseParams {
/**
* The name of the artboard to get the ViewModel from.
* If not provided, uses the default artboard.
*/
artboardName?: string;
/**
* The ViewModel instance name (uses `createInstanceByName()`).
* If not provided, creates the default instance.
*/
name?: string;
instanceName?: string;
}

/**
* Use the ViewModel assigned to the default artboard.
*/
interface UseViewModelInstanceFileDefault
extends UseViewModelInstanceFileBaseParams {
artboardName?: never;
viewModelName?: never;
}

/**
* Use the ViewModel assigned to a specific artboard.
*/
interface UseViewModelInstanceFileByArtboard
extends UseViewModelInstanceFileBaseParams {
/**
* Get the ViewModel assigned to this artboard.
*/
artboardName: string;
viewModelName?: never;
}

/**
* Use a ViewModel by name (file-wide lookup).
* ViewModels are defined at the file level, not per-artboard.
*/
interface UseViewModelInstanceFileByViewModelName
extends UseViewModelInstanceFileBaseParams {
artboardName?: never;
/**
* The name of the ViewModel to use (uses `viewModelByName()`).
* ViewModels are defined at the file level and looked up by name across the entire file.
*/
viewModelName: string;
}

export type UseViewModelInstanceFileParams =
| UseViewModelInstanceFileDefault
| UseViewModelInstanceFileByArtboard
| UseViewModelInstanceFileByViewModelName;

export interface UseViewModelInstanceViewModelParams
extends UseViewModelInstanceBaseParams {
/**
Expand Down Expand Up @@ -67,8 +102,9 @@ type CreateInstanceResult = {

function createInstance(
source: ViewModelSource | null,
name: string | undefined,
instanceName: string | undefined,
artboardName: string | undefined,
viewModelName: string | undefined,
useNew: boolean
): CreateInstanceResult {
if (!source) {
Expand All @@ -81,41 +117,53 @@ function createInstance(
}

if (isRiveFile(source)) {
const viewModel = source.defaultArtboardViewModel(
artboardName ? ArtboardByName(artboardName) : undefined
);
if (!viewModel) {
if (artboardName) {
let viewModel: ViewModel | undefined;
if (viewModelName) {
viewModel = source.viewModelByName(viewModelName);
if (!viewModel) {
return {
instance: null,
needsDispose: false,
error: `Artboard '${artboardName}' not found or has no ViewModel`,
error: `ViewModel '${viewModelName}' not found`,
};
}
return { instance: null, needsDispose: false };
} else {
viewModel = source.defaultArtboardViewModel(
artboardName ? ArtboardByName(artboardName) : undefined
);
if (!viewModel) {
if (artboardName) {
return {
instance: null,
needsDispose: false,
error: `Artboard '${artboardName}' not found or has no ViewModel`,
};
}
return { instance: null, needsDispose: false };
}
}
const vmi = name
? viewModel.createInstanceByName(name)
const vmi = instanceName
? viewModel.createInstanceByName(instanceName)
: viewModel.createDefaultInstance();
if (!vmi && name) {
if (!vmi && instanceName) {
return {
instance: null,
needsDispose: false,
error: `ViewModel instance '${name}' not found`,
error: `ViewModel instance '${instanceName}' not found`,
};
}
return { instance: vmi ?? null, needsDispose: true };
}

// ViewModel source
let vmi: ViewModelInstance | undefined;
if (name) {
vmi = source.createInstanceByName(name);
if (instanceName) {
vmi = source.createInstanceByName(instanceName);
if (!vmi) {
return {
instance: null,
needsDispose: false,
error: `ViewModel instance '${name}' not found`,
error: `ViewModel instance '${instanceName}' not found`,
};
}
} else if (useNew) {
Expand Down Expand Up @@ -144,7 +192,14 @@ function createInstance(
* ```tsx
* // From RiveFile with specific instance name
* const { riveFile } = useRiveFile(require('./animation.riv'));
* const instance = useViewModelInstance(riveFile, { name: 'PersonInstance' });
* const instance = useViewModelInstance(riveFile, { instanceName: 'PersonInstance' });
* ```
*
* @example
* ```tsx
* // From RiveFile with specific ViewModel name
* const { riveFile } = useRiveFile(require('./animation.riv'));
* const instance = useViewModelInstance(riveFile, { viewModelName: 'Settings' });
* ```
*
* @example
Expand Down Expand Up @@ -232,9 +287,14 @@ export function useViewModelInstance(
| UseViewModelInstanceViewModelParams
| UseViewModelInstanceRefParams
): ViewModelInstance | null {
const name = (params as UseViewModelInstanceFileParams | undefined)?.name;
const fileInstanceName = (params as { instanceName?: string } | undefined)
?.instanceName;
const viewModelInstanceName = (params as { name?: string } | undefined)?.name;
const instanceName = fileInstanceName ?? viewModelInstanceName;
const artboardName = (params as UseViewModelInstanceFileParams | undefined)
?.artboardName;
const viewModelName = (params as UseViewModelInstanceFileParams | undefined)
?.viewModelName;
const useNew =
(params as UseViewModelInstanceViewModelParams | undefined)?.useNew ??
false;
Expand All @@ -247,13 +307,19 @@ export function useViewModelInstance(
} | null>(null);

const result = useMemo(() => {
const created = createInstance(source, name, artboardName, useNew);
const created = createInstance(
source,
instanceName,
artboardName,
viewModelName,
useNew
);
if (created.instance && onInit) {
onInit(created.instance);
}
return created;
// eslint-disable-next-line react-hooks/exhaustive-deps -- onInit excluded intentionally
}, [source, name, artboardName, useNew]);
}, [source, instanceName, artboardName, viewModelName, useNew]);

// Dispose previous instance if it changed and needed disposal
if (
Expand Down
Loading