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
17 changes: 13 additions & 4 deletions android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ class HybridRiveFile : HybridRiveFileSpec() {
get() = riveFile?.viewModelCount?.toDouble()

override fun viewModelByIndex(index: Double): HybridViewModelSpec? {
val vm = riveFile?.getViewModelByIndex(index.toInt()) ?: return null
return HybridViewModel(vm)
if (index < 0) return null
return try {
val vm = riveFile?.getViewModelByIndex(index.toInt()) ?: return null
HybridViewModel(vm)
} catch (e: Exception) {
null
}
}

override fun viewModelByName(name: String): HybridViewModelSpec? {
val vm = riveFile?.getViewModelByName(name) ?: return null
return HybridViewModel(vm)
return try {
val vm = riveFile?.getViewModelByName(name) ?: return null
HybridViewModel(vm)
} catch (e: Exception) {
null
}
}

override fun defaultArtboardViewModel(artboardBy: ArtboardBy?): HybridViewModelSpec? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class HybridViewModel(private val viewModel: ViewModel) : HybridViewModelSpec()
get() = viewModel.name

override fun createInstanceByIndex(index: Double): HybridViewModelInstanceSpec? {
if (index < 0) return null
try {
val vmi = viewModel.createInstanceFromIndex(index.toInt())
return HybridViewModelInstance(vmi)
Expand Down
340 changes: 340 additions & 0 deletions example/__tests__/databinding-advanced.harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
import { describe, it, expect } from 'react-native-harness';
import type {
ViewModelInstance,
ViewModelStringProperty,
} from '@rive-app/react-native';
import { RiveFileFactory } from '@rive-app/react-native';

const DATABINDING = require('../assets/rive/databinding.riv');
const DATABINDING_LISTS = require('../assets/rive/databinding_lists.riv');
const DATABINDING_IMAGES = require('../assets/rive/databinding_images.riv');
const ARTBOARD_DB_TEST = require('../assets/rive/artboard_db_test.riv');

function expectDefined<T>(value: T): asserts value is NonNullable<T> {
expect(value).toBeDefined();
}

async function loadFile(source: number) {
return RiveFileFactory.fromSource(source, undefined);
}

describe('RiveFile ViewModel Access', () => {
it('viewModelCount returns expected count', async () => {
const file = await loadFile(DATABINDING);
expect(file.viewModelCount).toBe(2);
});

it('viewModelByIndex(0) returns a ViewModel', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByIndex(0);
expect(vm).toBeDefined();
});

it('viewModelByIndex(-1) returns undefined or throws', async () => {
const file = await loadFile(DATABINDING);
try {
const vm = file.viewModelByIndex(-1);
expect(vm).toBeUndefined();
} catch {
// Android Rive SDK throws a JNI exception for invalid indices
}
});

it('viewModelByIndex(100) returns undefined or throws', async () => {
const file = await loadFile(DATABINDING);
try {
const vm = file.viewModelByIndex(100);
expect(vm).toBeUndefined();
} catch {
// Android Rive SDK throws a JNI exception for out-of-range indices
}
});

it('viewModelByName("Person") returns a ViewModel', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expect(vm).toBeDefined();
expect(vm!.modelName).toBe('Person');
});

it('viewModelByName("DoesNotExist") returns undefined or throws', async () => {
const file = await loadFile(DATABINDING);
try {
const vm = file.viewModelByName('DoesNotExist');
expect(vm).toBeUndefined();
} catch {
// Android Rive SDK throws a JNI exception for non-existent names
}
});
});

describe('ViewModel Properties Metadata', () => {
it('Person VM has expected propertyCount and instanceCount', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expectDefined(vm);
// Legacy returns 8/2, experimental returns 0/0
expect(vm.propertyCount).toBeGreaterThanOrEqual(0);
expect(vm.instanceCount).toBeGreaterThanOrEqual(0);
});
});

describe('ViewModel Creation Variants', () => {
it('createInstanceByName("Gordon") works', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expectDefined(vm);

const instance = vm.createInstanceByName('Gordon');
expectDefined(instance);
});

it('createInstanceByName("DoesNotExist") returns undefined or throws', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expectDefined(vm);

// Legacy returns undefined, experimental throws
try {
const instance = vm.createInstanceByName('DoesNotExist');
expect(instance).toBeUndefined();
} catch {
// experimental backend throws - that's fine
}
});

it('createInstanceByIndex(0) works', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByIndex(0);
expectDefined(vm);

const instance = vm.createInstanceByIndex(0);
expectDefined(instance);
});

it('createInstanceByIndex(100) returns undefined or empty instance', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expectDefined(vm);

// Legacy returns undefined, experimental returns an empty instance
vm.createInstanceByIndex(100);
expect(true).toBe(true);
});

it('createDefaultInstance() works', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expectDefined(vm);

const instance = vm.createDefaultInstance();
expectDefined(instance);
});

it('createInstance() (blank) works', async () => {
const file = await loadFile(DATABINDING);
const vm = file.viewModelByName('Person');
expectDefined(vm);

const instance = vm.createInstance();
expectDefined(instance);
});
});

describe('List Properties', () => {
it('listProperty("team") returns defined property', async () => {
const file = await loadFile(DATABINDING_LISTS);
const vm = file.viewModelByName('DevRel');
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);
});

it('list length returns expected count', async () => {
const file = await loadFile(DATABINDING_LISTS);
const vm = file.viewModelByName('DevRel');
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);
expect(list.length).toBe(5);
});

it('getInstanceAt returns ViewModelInstances with correct names', async () => {
const file = await loadFile(DATABINDING_LISTS);
const vm = file.viewModelByName('DevRel');
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);

const names = ['Gordon', 'David', 'Tod', 'Erik', 'Adam'];
for (let i = 0; i < names.length; i++) {
const item: ViewModelInstance = list.getInstanceAt(i)!;
expectDefined(item);
const nameProp: ViewModelStringProperty = item.stringProperty('name')!;
expectDefined(nameProp);
expect(nameProp.value).toBe(names[i]);
}
});

it('addInstance increases length', async () => {
const file = await loadFile(DATABINDING_LISTS);
const devRelVM = file.viewModelByName('DevRel');
expectDefined(devRelVM);
const instance = devRelVM.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);
const initialLength = list.length;

const personVM = file.viewModelByName('Person');
expectDefined(personVM);
const newPerson = personVM.createInstance();
expectDefined(newPerson);
const nameProp = newPerson.stringProperty('name');
expectDefined(nameProp);
nameProp.value = 'Hernan';

list.addInstance(newPerson);
expect(list.length).toBe(initialLength + 1);

const added = list.getInstanceAt(list.length - 1);
expectDefined(added);
const addedName = added.stringProperty('name');
expectDefined(addedName);
expect(addedName.value).toBe('Hernan');
});

// These 3 list mutations crash the Rive experimental renderer
// (EXC_BAD_ACCESS in rive::CommandQueue::processMessages).
// They pass on the legacy backend. Skipping until the Rive engine fix.
it.skip('removeInstanceAt decreases length', async () => {
const file = await loadFile(DATABINDING_LISTS);
const vm = file.viewModelByName('DevRel');
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);
const initialLength = list.length;

list.removeInstanceAt(0);
expect(list.length).toBe(initialLength - 1);
});

it.skip('swap reorders items', async () => {
const file = await loadFile(DATABINDING_LISTS);
const vm = file.viewModelByName('DevRel');
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);

const name0Before = list.getInstanceAt(0)!.stringProperty('name')!.value;
const name1Before = list.getInstanceAt(1)!.stringProperty('name')!.value;

const result = list.swap(0, 1);
expect(result).toBe(true);

const name0After = list.getInstanceAt(0)!.stringProperty('name')!.value;
const name1After = list.getInstanceAt(1)!.stringProperty('name')!.value;

expect(name0After).toBe(name1Before);
expect(name1After).toBe(name0Before);
});

it.skip('addInstanceAt inserts at position', async () => {
const file = await loadFile(DATABINDING_LISTS);
const devRelVM = file.viewModelByName('DevRel');
expectDefined(devRelVM);
const instance = devRelVM.createDefaultInstance();
expectDefined(instance);

const list = instance.listProperty('team');
expectDefined(list);
const initialLength = list.length;

const personVM = file.viewModelByName('Person');
expectDefined(personVM);
const lancePerson = personVM.createInstance();
expectDefined(lancePerson);
lancePerson.stringProperty('name')!.value = 'Lance';

const result = list.addInstanceAt(lancePerson, 2);
expect(result).toBe(true);
expect(list.length).toBe(initialLength + 1);

const insertedName = list.getInstanceAt(2)!.stringProperty('name')!.value;
expect(insertedName).toBe('Lance');
});
});

// These two .riv files crash the Rive experimental renderer on load
// (EXC_BAD_ACCESS in rive::CommandQueue::processMessages).
// They pass on the legacy backend. Skipping until the Rive engine fix.
describe.skip('Artboard Properties', () => {
it('artboardProperty returns defined properties', async () => {
const file = await loadFile(ARTBOARD_DB_TEST);
const vm = file.defaultArtboardViewModel();
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const artboard1 = instance.artboardProperty('artboard_1');
expectDefined(artboard1);

const artboard2 = instance.artboardProperty('artboard_2');
expectDefined(artboard2);
});

it('getBindableArtboard returns a BindableArtboard with correct name', async () => {
const file = await loadFile(ARTBOARD_DB_TEST);
const artboardNames = file.artboardNames;
expect(artboardNames.length).toBeGreaterThan(0);

const bindable = file.getBindableArtboard(artboardNames[0]!);
expectDefined(bindable);
expect(bindable.artboardName).toBe(artboardNames[0]);
});

it('artboardProperty.set(bindable) does not throw', async () => {
const file = await loadFile(ARTBOARD_DB_TEST);
const vm = file.defaultArtboardViewModel();
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const artboardProp = instance.artboardProperty('artboard_1');
expectDefined(artboardProp);

const artboardNames = file.artboardNames;
const bindable = file.getBindableArtboard(artboardNames[0]!);

expect(() => artboardProp.set(bindable)).not.toThrow();
});
});

describe.skip('Image Properties', () => {
it('imageProperty("bound_image") returns defined property', async () => {
const file = await loadFile(DATABINDING_IMAGES);
const vm = file.viewModelByName('MyViewModel');
expectDefined(vm);
const instance = vm.createInstanceByIndex(0);
expectDefined(instance);

const imageProp = instance.imageProperty('bound_image');
expectDefined(imageProp);
});
});
Binary file added example/assets/rive/artboard_db_test.riv
Binary file not shown.
Binary file added example/assets/rive/databinding_images.riv
Binary file not shown.
Binary file added example/assets/rive/databinding_lists.riv
Binary file not shown.
6 changes: 3 additions & 3 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"@react-native-community/cli": "18.0.0",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native-harness/platform-android": "^1.0.0-alpha.20",
"@react-native-harness/platform-apple": "^1.0.0-alpha.20",
"@react-native-harness/platform-android": "^1.0.0-alpha.25",
"@react-native-harness/platform-apple": "^1.0.0-alpha.25",
"@react-native/babel-preset": "0.79.2",
"@react-native/metro-config": "0.79.2",
"@react-native/typescript-config": "0.79.2",
Expand All @@ -43,7 +43,7 @@
"babel-plugin-react-compiler": "^1.0.0",
"deep-equal": "^2.2.3",
"react-native-builder-bob": "^0.40.10",
"react-native-harness": "^1.0.0-alpha.20"
"react-native-harness": "^1.0.0-alpha.25"
},
"engines": {
"node": ">=18"
Expand Down
Loading
Loading