Files
rive-ios/Tests/RiveDelegatesTest.swift
dskuza 1b5de5fbba Force advancing on iOS if advancing by 0
Similar to Android: https://github.com/rive-app/rive/pull/8725

With changes from Option C, the C++ runtime will report that an animation should not continue if advancing by 0. This causes the iOS advance logic to break, and if stopping / pausing and then playing, the advance by 0 will continually return false, not allowing an animation to play again.

Diffs=
d527114521 Force advancing on iOS if advancing by 0 (#8766)

Co-authored-by: David Skuza <david@rive.app>
2024-12-19 15:09:27 +00:00

422 lines
17 KiB
Swift

//
// RiveDelegatesTest.swift
// RiveRuntimeTests
//
// Created by Maxwell Talbot on 12/05/2021.
// Copyright © 2021 Rive. All rights reserved.
//
import XCTest
import RiveRuntime
extension RiveFile {
convenience init(testfileName: String, extension ext: String = ".riv") throws {
let byteArray = RiveFile.getBytes(fileName: testfileName, extension: ext)
try self.init(byteArray: byteArray, loadCdn: false)
}
static func getBytes(fileName: String, extension ext: String = ".riv") -> [UInt8] {
guard let url = Bundle(for: DelegatesTest.self).url(forResource: fileName, withExtension: ext) else {
fatalError("Failed to locate \(fileName) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(url) from bundle.")
}
// Import the data into a RiveFile
return [UInt8](data)
}
}
class DrDelegate: RivePlayerDelegate, RiveStateMachineDelegate {
var stateMachinePlays = [String]()
var stateMachinePauses = [String]()
var stateMachineStops = [String]()
var linearAnimaitonPlays = [String]()
var linearAnimaitonPauses = [String]()
var linearAnimaitonStops = [String]()
var loops = [String]()
var stateMachineNames = [String]()
var stateMachineStates = [String]()
var events = [RiveEvent]()
func player(playedWithModel riveModel: RiveModel?) {
if let stateMachineName = riveModel?.stateMachine?.name() {
stateMachinePlays.append(stateMachineName)
}
else if let animationName = riveModel?.animation?.name() {
linearAnimaitonPlays.append(animationName)
}
}
func player(pausedWithModel riveModel: RiveModel?) {
if let stateMachineName = riveModel?.stateMachine?.name() {
stateMachinePauses.append(stateMachineName)
}
else if let animationName = riveModel?.animation?.name() {
linearAnimaitonPauses.append(animationName)
}
}
func player(loopedWithModel riveModel: RiveModel?, type: Int) {
if let stateMachineName = riveModel?.stateMachine?.name() {
loops.append(stateMachineName)
}
else if let animationName = riveModel?.animation?.name() {
loops.append(animationName)
}
}
func player(stoppedWithModel riveModel: RiveModel?) {
if let stateMachineName = riveModel?.stateMachine?.name() {
stateMachineStops.append(stateMachineName)
}
else if let animationName = riveModel?.animation?.name() {
linearAnimaitonStops.append(animationName)
}
}
func player(didAdvanceby seconds: Double, riveModel: RiveModel?) { }
func stateMachine(_ stateMachine: RiveStateMachineInstance, didChangeState stateName: String) {
stateMachineNames.append(stateMachine.name())
stateMachineStates.append(stateName)
}
func onRiveEventReceived(onRiveEvent riveEvent: RiveEvent) {
events.append(riveEvent)
}
}
// Technical Note:
// We manually call view.advance(0) in these tests because the results are based on
// messages received by a delegate which is triggered by a timer. We can't wait for
// the timer so we trigger it manually
class DelegatesTest: XCTestCase {
func testPlay() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one")
// This is necessary because play starts a timer that will eventually get to
// .advance(n) which triggers the event queue which sends word to the delegate...
// But the assert happens too fast, so we need to advance manually beforehand.
view.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonPlays.count, 1)
}
func testPlayTwice() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one")
viewModel.play(animationName: "one")
view.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonPlays.count, 2)
}
func testPause() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one")
viewModel.pause()
view.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonPauses.count, 1)
}
func testPauseWhenNotPlaying() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.pause()
view.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonPauses.count, 0)
}
func testStop() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one")
viewModel.stop()
view.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonStops.count, 1)
}
func testStopNotMounted() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, animationName: "one", autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.stop()
view.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonStops.count, 1)
}
func testStopPaused() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one")
viewModel.pause()
viewModel.stop()
view.advance(delta: 0)
viewModel.riveView?.advance(delta: 0)
XCTAssertEqual(delegate.linearAnimaitonStops.count, 1)
}
func testLoopOneShot() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one", loop: .oneShot)
view.advance(delta: Double(viewModel.riveModel!.animation!.effectiveDurationInSeconds()+0.1))
XCTAssertEqual(delegate.loops.count, 0)
XCTAssertEqual(delegate.linearAnimaitonPlays.count, 1)
XCTAssertEqual(delegate.linearAnimaitonPauses.count, 1)
XCTAssertEqual(delegate.linearAnimaitonStops.count, 0)
}
func testLoopLoop() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one", loop: .loop)
view.advance(delta: Double(viewModel.riveModel!.animation!.effectiveDurationInSeconds()+0.1))
XCTAssertEqual(delegate.loops.count, 1)
XCTAssertEqual(delegate.linearAnimaitonPlays.count, 1)
XCTAssertEqual(delegate.linearAnimaitonPauses.count, 0)
XCTAssertEqual(delegate.linearAnimaitonStops.count, 0)
}
func testLoopPingPong() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "multiple_animations")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
viewModel.play(animationName: "one", loop: .pingPong)
view.advance(delta: Double(viewModel.riveModel!.animation!.effectiveDurationInSeconds()+0.1))
XCTAssertEqual(delegate.loops.count, 1)
XCTAssertEqual(delegate.linearAnimaitonPlays.count, 1)
XCTAssertEqual(delegate.linearAnimaitonPauses.count, 0)
XCTAssertEqual(delegate.linearAnimaitonStops.count, 0)
}
func testGeneralEventsReported() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "rating_animation")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, stateMachineName: "State Machine 1", autoPlay: true)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
view.stateMachineDelegate = delegate
viewModel.setInput("rating", value: 2.0)
view.advance(delta:0.0)
view.advance(delta:0.1)
XCTAssertEqual(delegate.events.count, 1)
XCTAssertEqual(delegate.events.first?.name(), "rating2")
XCTAssertTrue(delegate.events.first is RiveGeneralEvent)
viewModel.setInput("rating", value: 5.0)
view.advance(delta:0.0)
view.advance(delta:0.1)
XCTAssertEqual(delegate.events.count, 3)
XCTAssertEqual(delegate.events.last?.name(), "gotorive")
XCTAssertTrue(delegate.events.last is RiveOpenUrlEvent)
XCTAssertTrue((delegate.events.last as? RiveOpenUrlEvent)?.url != nil)
}
func testNestedStateMachineLooping() throws {
// this test tests advancing a state machine with a one shot animation that ends after 1 second
// but the artboard has a nested artboard that is playing a looping animation via its state machine.
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "ball_test")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, stateMachineName: "State Machine 1", autoPlay: true)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
view.stateMachineDelegate = delegate
XCTAssertEqual(true, viewModel.isPlaying)
view.advance(delta:0.1)
XCTAssertEqual(true, viewModel.isPlaying)
view.advance(delta:1.0)
XCTAssertEqual(true, viewModel.isPlaying)
view.advance(delta:0.1)
XCTAssertEqual(true, viewModel.isPlaying)
}
func testNestedStateMachineOneShot() throws {
// this test tests advancing a state machine with a one shot animation that ends after 1 second
// but the artboard has a nested artboard that is playing a one shot animation via its state machine.
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "ball_test")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(
model,
stateMachineName: "State Machine 1",
autoPlay: true,
artboardName: "Artboard 2"
)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
view.stateMachineDelegate = delegate
XCTAssertEqual(true, viewModel.isPlaying)
view.advance(delta:0.1)
XCTAssertEqual(true, viewModel.isPlaying)
view.advance(delta:1.0)
// One more advance to consume the last event.
view.advance(delta:0.1)
XCTAssertEqual(true, viewModel.isPlaying)
// One more advance to settle.
view.advance(delta:0.1)
XCTAssertEqual(false, viewModel.isPlaying)
}
func testStateMachineLayerStates() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "what_a_state")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, stateMachineName: "State Machine 2", autoPlay: true)
let view = viewModel.createRiveView()
view.playerDelegate = delegate
view.stateMachineDelegate = delegate
view.advance(delta:0.1)
XCTAssertEqual(delegate.stateMachinePlays.count, 1)
XCTAssertEqual(delegate.stateMachineStates.count, 1)
XCTAssertEqual(delegate.stateMachineNames[0], "State Machine 2")
XCTAssertEqual(delegate.stateMachineStates[0], "go right")
view.advance(delta:1.1)
XCTAssertEqual(delegate.stateMachineStates.count, 2)
XCTAssertEqual(delegate.stateMachineNames[1], "State Machine 2")
XCTAssertEqual(delegate.stateMachineStates[1], "ExitState")
// takes an extra advance to trigger
view.advance(delta:0.1)
XCTAssertEqual(delegate.stateMachinePauses.count, 1)
}
func testStateMachineLayerStatesComplex() throws {
let delegate = DrDelegate()
let file = try RiveFile(testfileName: "what_a_state")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, stateMachineName: "State Machine 1", autoPlay: true)
let view = viewModel.createRiveView()
view.stateMachineDelegate = delegate
view.advance(delta:0.0)
XCTAssertEqual(delegate.stateMachineStates.count, 0)
viewModel.play()
// MARK: Input
// lets just start, expect 1 change.
viewModel.triggerInput("right")
// TODO: looks like we got a bit of a bug here
// If we do not call this advance, the first animation doesnt seem to get the delta applied.
view.advance(delta:0.0)
view.advance(delta:0.4)
XCTAssertEqual(delegate.stateMachineStates.count, 1)
XCTAssertEqual(delegate.stateMachineStates[0], "go right")
XCTAssertEqual(delegate.stateMachineNames.count, 1)
XCTAssertEqual(delegate.stateMachineNames[0], "State Machine 1")
delegate.stateMachineStates.removeAll()
// should be in same animation still. no state change
view.advance(delta:0.4)
XCTAssertEqual(0, delegate.stateMachineStates.count)
XCTAssertEqual(true, viewModel.isPlaying)
// state machine advance ends, but
// artboard advanced in this period, so we "keepGoing"
view.advance(delta:0.4)
XCTAssertEqual(true, viewModel.isPlaying)
// animation is done
view.advance(delta:0.1)
XCTAssertEqual(false, viewModel.isPlaying)
XCTAssertEqual(0, delegate.stateMachineStates.count)
// animation is just kinda stuck there. no change no happening.
view.advance(delta:0.4)
XCTAssertEqual(false, viewModel.isPlaying)
XCTAssertEqual(0, delegate.stateMachineStates.count)
// MARK: Input
// ok lets change thigns up again.
viewModel.triggerInput("change")
view.advance(delta:0.0)
view.advance(delta:0.4)
XCTAssertEqual(true, viewModel.isPlaying)
XCTAssertEqual(1, delegate.stateMachineStates.count)
XCTAssertEqual("change!", delegate.stateMachineStates[0])
delegate.stateMachineStates.removeAll()
// as before lets advance inside the animation -> no change
view.advance(delta:0.4)
XCTAssertEqual(true, viewModel.isPlaying)
XCTAssertEqual(0, delegate.stateMachineStates.count)
// as before lets advance beyond the end of the animaiton, in this case change to exit!
view.advance(delta:0.4)
// Is playing will be true one last time because there was a change of state
XCTAssertEqual(true, viewModel.isPlaying)
XCTAssertEqual(1, delegate.stateMachineStates.count)
XCTAssertEqual("ExitState", delegate.stateMachineStates[0])
delegate.stateMachineStates.removeAll()
// chill on exit. no change.
view.advance(delta:0.4)
XCTAssertEqual(false, viewModel.isPlaying)
XCTAssertEqual(0, delegate.stateMachineStates.count)
}
}