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
37 changes: 34 additions & 3 deletions example/__tests__/hooks.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ type UseRiveNumberContext = {
value: number | undefined;
error: Error | null;
setValue: ((v: number) => void) | null;
renderValues: (number | undefined)[];
};

function createUseRiveNumberContext(): UseRiveNumberContext {
return { value: undefined, error: null, setValue: null };
return { value: undefined, error: null, setValue: null, renderValues: [] };
}

type UseViewModelInstanceContext = {
instance: ViewModelInstance | null;
instance: ViewModelInstance | null | undefined;
age: number | undefined;
};

Expand All @@ -50,6 +51,8 @@ function UseRiveNumberTestComponent({
}) {
const { value, setValue, error } = useRiveNumber('health', instance);

context.renderValues.push(value);

useEffect(() => {
context.value = value;
context.error = error;
Expand All @@ -70,7 +73,7 @@ function UseViewModelInstanceTestComponent({
file: RiveFile;
context: UseViewModelInstanceContext;
}) {
const instance = useViewModelInstance(file);
const { instance } = useViewModelInstance(file);

const age = useMemo(() => {
if (!instance) return undefined;
Expand All @@ -96,6 +99,34 @@ function expectDefined<T>(value: T): asserts value is NonNullable<T> {
}

describe('useRiveNumber Hook', () => {
it('starts undefined then receives value via listener', async () => {
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
const vm = file.defaultArtboardViewModel();
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);

const context = createUseRiveNumberContext();

await render(
<UseRiveNumberTestComponent instance={instance} context={context} />
);

// First render must produce undefined — not a synchronous read from property.value
expect(context.renderValues[0]).toBeUndefined();

// After listener fires, value should be a number
await waitFor(
() => {
expect(context.error).toBeNull();
expect(typeof context.value).toBe('number');
},
{ timeout: 5000 }
);

cleanup();
});

it('returns value from number property', async () => {
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
const vm = file.defaultArtboardViewModel();
Expand Down
9 changes: 7 additions & 2 deletions example/src/demos/DataBindingArtboardsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
return (
<View style={styles.container}>
<Text style={styles.errorText}>
{error || 'Failed to load Rive files'}
{error?.message || 'Failed to load Rive files'}
</Text>
</View>
);
Expand All @@ -78,7 +78,7 @@
mainFile: RiveFile;
assetsFile: RiveFile;
}) {
const instance = useViewModelInstance(mainFile);
const { instance, error } = useViewModelInstance(mainFile);
const [currentArtboard, setCurrentArtboard] = useState<string>('Dragon');
const initializedRef = useRef(false);

Expand All @@ -98,6 +98,11 @@
}
}, [instance, assetsFile]);

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 103 in example/src/demos/DataBindingArtboardsExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

// Map display names to actual artboard names
const artboardOptions = [
{ label: 'Dragon', artboard: 'Character 1', fromAssets: true },
Expand Down
2 changes: 1 addition & 1 deletion example/src/demos/QuickStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function QuickStart() {
require('../../assets/rive/quick_start.riv')
);
const { riveViewRef, setHybridRef } = useRive();
const viewModelInstance = useViewModelInstance(riveFile, {
const { instance: viewModelInstance } = useViewModelInstance(riveFile, {
onInit: (vmi) => vmi.numberProperty('health')!.set(9),
});

Expand Down
6 changes: 4 additions & 2 deletions example/src/exercisers/FontFallbackExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ function MountedView({ text }: { text: string }) {
// https://rive.app/marketplace/26480-49641-simple-test-text-property/
require('../../assets/rive/font_fallback.riv')
);
const instance = useViewModelInstance(riveFile ?? null);
const { instance } = useViewModelInstance(riveFile);

const { setValue: setRiveText, error: textError } = useRiveString(
TEXT_PROPERTY,
Expand All @@ -285,7 +285,9 @@ function MountedView({ text }: { text: string }) {
if (error || !riveFile) {
return (
<View style={styles.riveContainer}>
<Text style={styles.errorText}>{error || 'Failed to load file'}</Text>
<Text style={styles.errorText}>
{error?.message || 'Failed to load file'}
</Text>
</View>
);
}
Expand Down
11 changes: 9 additions & 2 deletions example/src/exercisers/MenuListExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,21 @@
) : riveFile ? (
<MenuList file={riveFile} />
) : (
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
<Text style={styles.errorText}>
{error?.message || 'Unexpected error'}
</Text>
)}
</View>
);
}

function MenuList({ file }: { file: RiveFile }) {
const instance = useViewModelInstance(file, { required: true });
const { instance, error } = useViewModelInstance(file);

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 48 in example/src/exercisers/MenuListExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

if (!instance) {
return <ActivityIndicator size="large" color="#007AFF" />;
Expand Down
11 changes: 9 additions & 2 deletions example/src/exercisers/NestedViewModelExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@
) : riveFile ? (
<WithViewModelSetup file={riveFile} />
) : (
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
<Text style={styles.errorText}>
{error?.message || 'Unexpected error'}
</Text>
)}
</View>
);
}

function WithViewModelSetup({ file }: { file: RiveFile }) {
const instance = useViewModelInstance(file);
const { instance, error } = useViewModelInstance(file);

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 47 in example/src/exercisers/NestedViewModelExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

if (!instance) {
return <ActivityIndicator size="large" color="#0000ff" />;
Expand Down
4 changes: 2 additions & 2 deletions example/src/exercisers/OutOfBandAssets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export default function OutOfBandAssetsExample() {

if (isLoading) {
return <ActivityIndicator />;
} else if (error != null) {
} else if (error) {
return (
<View style={styles.safeAreaViewContainer}>
<Text>Error loading Rive file</Text>
<Text>Error loading Rive file: {error.message}</Text>
</View>
);
}
Expand Down
4 changes: 2 additions & 2 deletions example/src/exercisers/OutOfBandAssetsWithSuspense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ function RiveContent({ imageUrl }: { imageUrl: ImageURLS }) {

if (isLoading) {
return <ActivityIndicator />;
} else if (error != null) {
} else if (error) {
return (
<View style={styles.safeAreaViewContainer}>
<Text>Error loading Rive file: {error}</Text>
<Text>Error loading Rive file: {error.message}</Text>
</View>
);
}
Expand Down
2 changes: 1 addition & 1 deletion example/src/exercisers/ResponsiveLayouts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function ResponsiveLayoutsExample() {
{isLoading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : error ? (
<Text style={styles.errorText}>{error}</Text>
<Text style={styles.errorText}>{error.message}</Text>
) : riveFile ? (
<RiveView
hybridRef={{ f: (ref) => (riveRef.current = ref) }}
Expand Down
11 changes: 9 additions & 2 deletions example/src/exercisers/RiveDataBindingExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,22 @@
) : riveFile ? (
<WithViewModelSetup file={riveFile} />
) : (
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
<Text style={styles.errorText}>
{error?.message || 'Unexpected error'}
</Text>
)}
</View>
</View>
);
}

function WithViewModelSetup({ file }: { file: RiveFile }) {
const instance = useViewModelInstance(file);
const { instance, error } = useViewModelInstance(file);

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 44 in example/src/exercisers/RiveDataBindingExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

if (!instance) {
return <ActivityIndicator size="large" color="#0000ff" />;
Expand Down
4 changes: 3 additions & 1 deletion example/src/exercisers/RiveDataBindingExampleExpApi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export default function WithRiveFile() {
) : riveFile ? (
<WithViewModelSetup file={riveFile} />
) : (
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
<Text style={styles.errorText}>
{error?.message || 'Unexpected error'}
</Text>
)}
</View>
</View>
Expand Down
2 changes: 1 addition & 1 deletion example/src/exercisers/RiveEventsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function EventsExample() {
{isLoading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : error ? (
<Text style={styles.errorText}>{error}</Text>
<Text style={styles.errorText}>{error.message}</Text>
) : riveFile ? (
<RiveView
style={styles.rive}
Expand Down
2 changes: 1 addition & 1 deletion example/src/exercisers/RiveFileLoadingExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const CustomRiveView = ({ loadingMethod, title }: CustomRiveViewProps) => {
{!input || isLoading ? (
<ActivityIndicator style={styles.rive} size="large" color="#0000ff" />
) : error ? (
<Text style={styles.errorText}>{error}</Text>
<Text style={styles.errorText}>{error.message}</Text>
) : riveFile ? (
<RiveView
style={styles.rive}
Expand Down
2 changes: 1 addition & 1 deletion example/src/exercisers/RiveStateMachineInputsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function StateMachineInputsExample() {
{isLoading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : error ? (
<Text style={styles.errorText}>{error}</Text>
<Text style={styles.errorText}>{error.message}</Text>
) : riveFile ? (
<RiveView
style={styles.rive}
Expand Down
2 changes: 1 addition & 1 deletion example/src/exercisers/RiveTextRunExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function TextRunExample() {
{isLoading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : error ? (
<Text style={styles.errorText}>{error}</Text>
<Text style={styles.errorText}>{error.message}</Text>
) : riveFile ? (
<RiveView
style={styles.rive}
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/__tests__/useRiveFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('useRiveFile - input stability', () => {
);

await waitFor(() => {
expect((result.current as any).isLoading).toBe(false);
expect(result.current.isLoading).toBe(false);
});

const callCountBefore = (global as any).mockRiveFileFactory.fromURL.mock
Expand Down Expand Up @@ -120,7 +120,7 @@ describe('useRiveFile - updateReferencedAssets', () => {
);

await waitFor(() => {
expect((result.current as any).isLoading).toBe(false);
expect(result.current.isLoading).toBe(false);
});

expect(mockRiveFile.updateReferencedAssets).not.toHaveBeenCalled();
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('useRiveFile - updateReferencedAssets', () => {
);

await waitFor(() => {
expect((result.current as any).isLoading).toBe(false);
expect(result.current.isLoading).toBe(false);
});

const updatedAssets = {
Expand Down Expand Up @@ -189,7 +189,7 @@ describe('useRiveFile - updateReferencedAssets', () => {
);

await waitFor(() => {
expect((result.current as any).isLoading).toBe(false);
expect(result.current.isLoading).toBe(false);
});

rerender({ referencedAssets: assets });
Expand Down
9 changes: 8 additions & 1 deletion src/hooks/__tests__/useRiveProperty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ describe('useRiveProperty', () => {
},
addListener: jest.fn((callback: (value: string) => void) => {
listener = callback;
// Emit the current value immediately on subscribe, matching native behaviour:
// iOS legacy emits synchronously; experimental backend emits via valueStream.
callback(currentValue);
return () => {
listener = null;
};
Expand All @@ -36,7 +39,9 @@ describe('useRiveProperty', () => {
} as unknown as ViewModelInstance;
};

it('should return initial value from property on first render', () => {
it('should return initial value delivered via listener (not from a sync read)', () => {
// Hooks always start undefined; the listener emits the current value immediately
// on subscribe (synchronously for legacy, via stream for experimental).
const mockProperty = createMockProperty('Tea');
const mockInstance = createMockViewModelInstance({
'favDrink/type': mockProperty,
Expand All @@ -48,6 +53,8 @@ describe('useRiveProperty', () => {
})
);

// The mock's addListener emits 'Tea' synchronously — React batches it with the
// effect, so the value is available after renderHook (which wraps in act()).
const [value] = result.current;
expect(value).toBe('Tea');
});
Expand Down
Loading
Loading