Skip to content
Draft
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
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,45 @@ jobs:
echo "=== System log for Metro/Node ==="
xcrun simctl spawn booted log show --predicate 'process CONTAINS "node" OR process CONTAINS "metro"' --last 5m --style compact 2>&1 | tail -50 || true

- name: Debug - Collect iOS crash reports
if: failure() || cancelled()
run: |
set +e
UDID=$(xcrun simctl list devices | awk -F '[()]' '/Booted/{print $2; exit}')
echo "Booted simulator UDID: ${UDID:-<none>}"
mkdir -p crash-reports
# Simulator app crashes are written to the host's DiagnosticReports by
# ReportCrash, and sometimes inside the simulator's own data dir. Grab
# RiveExample-named reports plus anything from the last 25 min as a fallback.
for dir in \
"$HOME/Library/Logs/DiagnosticReports" \
"$HOME/Library/Developer/CoreSimulator/Devices/$UDID/data/Library/Logs/DiagnosticReports"; do
[ -d "$dir" ] || continue
find "$dir" -type f \
\( -iname 'RiveExample-*' -o \( \( -iname '*.ips' -o -iname '*.crash' \) -mmin -25 \) \) \
-exec cp {} crash-reports/ \; 2>/dev/null
done
count=$(ls -1 crash-reports 2>/dev/null | wc -l | tr -d ' ')
echo "=== Collected ${count} crash report(s) ==="
for f in crash-reports/*; do
[ -e "$f" ] || continue
echo "---------------- ${f} ----------------"
cat "$f"
echo
done
if [ "${count}" = "0" ]; then
echo "No crash reports found (app may not have crashed, or reports were not flushed yet)."
fi
exit 0

- name: Upload iOS crash reports
if: failure() || cancelled()
uses: actions/upload-artifact@v4
with:
name: ios-harness-crash-reports
path: crash-reports/
if-no-files-found: ignore

test-harness-android:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -618,6 +657,45 @@ jobs:
echo "=== Checking simulator logs for errors ==="
xcrun simctl spawn booted log show --predicate 'processImagePath CONTAINS "RiveExample"' --last 5m --style compact 2>&1 | tail -200 || echo "No logs found"

- name: Debug - Collect iOS crash reports
if: failure() || cancelled()
run: |
set +e
UDID=$(xcrun simctl list devices | awk -F '[()]' '/Booted/{print $2; exit}')
echo "Booted simulator UDID: ${UDID:-<none>}"
mkdir -p crash-reports
# Simulator app crashes are written to the host's DiagnosticReports by
# ReportCrash, and sometimes inside the simulator's own data dir. Grab
# RiveExample-named reports plus anything from the last 25 min as a fallback.
for dir in \
"$HOME/Library/Logs/DiagnosticReports" \
"$HOME/Library/Developer/CoreSimulator/Devices/$UDID/data/Library/Logs/DiagnosticReports"; do
[ -d "$dir" ] || continue
find "$dir" -type f \
\( -iname 'RiveExample-*' -o \( \( -iname '*.ips' -o -iname '*.crash' \) -mmin -25 \) \) \
-exec cp {} crash-reports/ \; 2>/dev/null
done
count=$(ls -1 crash-reports 2>/dev/null | wc -l | tr -d ' ')
echo "=== Collected ${count} crash report(s) ==="
for f in crash-reports/*; do
[ -e "$f" ] || continue
echo "---------------- ${f} ----------------"
cat "$f"
echo
done
if [ "${count}" = "0" ]; then
echo "No crash reports found (app may not have crashed, or reports were not flushed yet)."
fi
exit 0

- name: Upload iOS crash reports
if: failure() || cancelled()
uses: actions/upload-artifact@v4
with:
name: ios-harness-legacy-crash-reports
path: crash-reports/
if-no-files-found: ignore

test-harness-android-legacy:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
10 changes: 9 additions & 1 deletion android/src/new/java/com/margelo/nitro/rive/HybridRiveFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,15 @@ class HybridRiveFile(
Artboard.fromFile(file)
}
val vmSource = ViewModelSource.DefaultForArtboard(artboard)
val vmInfo = file.getDefaultViewModelInfo(artboard)
// getDefaultViewModelInfo throws when the artboard has no default
// ViewModel — a normal state for VM-less files (issue #189 fixture), not
// a failure. Resolve null like the legacy backend so callers get the
// documented "no ViewModel" result instead of a raw error.
val vmInfo = try {
file.getDefaultViewModelInfo(artboard)
} catch (e: Exception) {
return null
}
return HybridViewModel(file, riveWorker, vmInfo.viewModelName, this, vmSource)
}

Expand Down
14 changes: 11 additions & 3 deletions example/__tests__/autoplay.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,17 @@ describe('Auto dataBind with no default ViewModel (issue #189)', () => {

expect(context.error).toBeNull();

const vmi = context.ref!.getViewModelInstance();
expect(vmi).not.toBeNull();
expect(vmi!.numberProperty('ypos')).toBeDefined();
// The default ViewModel instance and its properties resolve asynchronously a
// short time after the view ref is assigned, so poll until the property is
// available rather than reading it the instant the ref exists.
await waitFor(
() => {
expect(
context.ref!.getViewModelInstance()?.numberProperty('ypos')
).toBeDefined();
},
{ timeout: 5000 }
);

cleanup();
});
Expand Down
32 changes: 22 additions & 10 deletions example/__tests__/reconfigure.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Fit,
type RiveFile,
type RiveViewRef,
type ViewModelNumberProperty,
} from '@rive-app/react-native';

const BOUNCING_BALL = require('../assets/rive/bouncing_ball.riv');
Expand Down Expand Up @@ -74,11 +75,17 @@ describe('RiveView reconfigure (file switch)', () => {
await render(<Wrapper />);
await waitFor(() => expect(context.ref).not.toBeNull(), { timeout: 5000 });

// Confirm bouncing_ball is animating via its ypos ViewModel property
const vmi1 = context.ref!.getViewModelInstance();
expect(vmi1).not.toBeNull();
const ypos1 = vmi1!.numberProperty('ypos');
expect(ypos1).toBeDefined();
// The ViewModel instance and its properties resolve asynchronously a short
// time after the view ref is assigned, so poll until the property is
// available rather than reading it the instant the ref exists.
let ypos1: ViewModelNumberProperty | undefined;
await waitFor(
() => {
ypos1 = context.ref!.getViewModelInstance()?.numberProperty('ypos');
expect(ypos1).toBeDefined();
},
{ timeout: 5000 }
);
const valueBefore = ypos1!.value;
await delay(500);
expect(ypos1!.value).not.toBe(valueBefore);
Expand All @@ -93,11 +100,16 @@ describe('RiveView reconfigure (file switch)', () => {
await delay(600);
expect(context.error).toBeNull();

// Animation should still be running on the reconfigured view
const vmi2 = context.ref!.getViewModelInstance();
expect(vmi2).not.toBeNull();
const ypos2 = vmi2!.numberProperty('ypos');
expect(ypos2).toBeDefined();
// Animation should still be running on the reconfigured view. The
// reconfigured instance also resolves asynchronously, so poll for it too.
let ypos2: ViewModelNumberProperty | undefined;
await waitFor(
() => {
ypos2 = context.ref!.getViewModelInstance()?.numberProperty('ypos');
expect(ypos2).toBeDefined();
},
{ timeout: 5000 }
);
const valueAfterSwitch = ypos2!.value;
await delay(500);
expect(ypos2!.value).not.toBe(valueAfterSwitch);
Expand Down
Loading
Loading