| |
| #include "catch.hpp" |
| #include "scripting_test_utilities.hpp" |
| #include "rive/lua/rive_lua_libs.hpp" |
| #include "rive/animation/state_machine_instance.hpp" |
| #include "rive/assets/image_asset.hpp" |
| #include "rive_file_reader.hpp" |
| #include <string> |
| |
| using namespace rive; |
| |
| // Test helper: ScriptedObject with an associated ScriptAsset that references a |
| // File |
| class ScriptedObjectWithFile : public ScriptedObject |
| { |
| bool addScriptedDirt(ComponentDirt value, bool recurse = false) override |
| { |
| return true; |
| } |
| bool m_markedToUpdate = false; |
| ScriptProtocol scriptProtocol() override { return ScriptProtocol::utility; } |
| void markNeedsUpdate() override { m_markedToUpdate = true; } |
| Component* component() override { return nullptr; } |
| |
| rcp<ScriptAsset> m_scriptAsset; |
| |
| public: |
| void setFileForScriptAsset(File* file) |
| { |
| m_scriptAsset = make_rcp<ScriptAsset>(); |
| m_scriptAsset->file(file); |
| setAsset(m_scriptAsset); |
| } |
| uint32_t assetId() override |
| { |
| return m_scriptAsset ? m_scriptAsset->assetId() : 0; |
| } |
| bool needsUpdate() { return m_markedToUpdate; } |
| }; |
| |
| TEST_CASE("Scripted Context markNeedsUpdate works", "[scripting]") |
| { |
| ScriptingTest vm( |
| R"( |
| |
| -- Called once when the script initializes. |
| function init(self: MyNode, context: Context): boolean |
| context:markNeedsUpdate() |
| return true |
| end |
| |
| )"); |
| ScriptedObjectTest scriptedObjectTest; |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| { |
| lua_getglobal(L, "init"); |
| lua_pushvalue(L, -2); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 2, 1, 0) == LUA_OK); |
| rive_lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| CHECK(scriptedObjectTest.needsUpdate()); |
| } |
| } |
| |
| TEST_CASE("Scripted Context errors when used after disposal", "[scripting]") |
| { |
| ScriptingTest vm( |
| R"( |
| function callMarkNeedsUpdate(context: Context) |
| context:markNeedsUpdate() |
| end |
| )"); |
| ScriptedObjectTest scriptedObjectTest; |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| lua_getglobal(L, "callMarkNeedsUpdate"); |
| auto scriptedContext = lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| scriptedContext->clearScriptedObject(); |
| int result = lua_pcall(L, 1, 0, 0); |
| REQUIRE(result == LUA_ERRRUN); |
| const char* error = lua_tostring(L, -1); |
| REQUIRE(error != nullptr); |
| CHECK(std::string(error).find( |
| "context:markNeedsUpdate() called on a disposed context") != |
| std::string::npos); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| TEST_CASE("script has access to user created view models via Data", "[silver]") |
| { |
| rive::SerializingFactory silver; |
| auto file = |
| ReadRiveFile("assets/script_create_viewmodel_instance.riv", &silver); |
| auto artboard = file->artboardNamed("main"); |
| |
| silver.frameSize(artboard->width(), artboard->height()); |
| REQUIRE(artboard != nullptr); |
| auto stateMachine = artboard->stateMachineAt(0); |
| int viewModelId = artboard.get()->viewModelId(); |
| |
| auto vmi = viewModelId == -1 |
| ? file->createViewModelInstance(artboard.get()) |
| : file->createViewModelInstance(viewModelId, 0); |
| stateMachine->bindViewModelInstance(vmi); |
| stateMachine->advanceAndApply(0.1f); |
| |
| auto renderer = silver.makeRenderer(); |
| artboard->draw(renderer.get()); |
| |
| // Push element |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("newButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| // Push element at specific index |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("newAtButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| // Swap elements from indexes |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("swapButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| // Shift first element |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("shiftButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| // Pop last element |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("popButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| // Pop all elements and pop beyond empty list |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("popButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| trigger->trigger(); |
| trigger->trigger(); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| // Push 2 elements |
| { |
| silver.addFrame(); |
| rive::ViewModelInstanceViewModel* button = |
| vmi->propertyValue("newButton") |
| ->as<rive::ViewModelInstanceViewModel>(); |
| REQUIRE(button != nullptr); |
| auto instance = button->referenceViewModelInstance(); |
| rive::ViewModelInstanceTrigger* trigger = |
| instance->propertyValue("onClick") |
| ->as<rive::ViewModelInstanceTrigger>(); |
| REQUIRE(trigger != nullptr); |
| trigger->trigger(); |
| trigger->trigger(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| } |
| |
| CHECK(silver.matches("script_create_viewmodel_instance")); |
| } |
| |
| // Regression test for rive-ios#454: when a ScriptingVM is supplied to |
| // File::import (as the CommandServer does on iOS), the Lua `Data` global — |
| // which exposes view model constructors like Data.ProbeChipVM.new() — must |
| // still be initialized. Before the fix, initializeLuaData only ran inside |
| // makeScriptingVM(), which is skipped when an external VM is provided, so |
| // `Data` was nil in scripts. |
| TEST_CASE("Data global is initialized when a ScriptingVM is provided to import", |
| "[scripting]") |
| { |
| // Mirror CommandServer::processCommands: create the VM up front and pass |
| // it into File::import. |
| auto context = |
| std::make_unique<rive::CPPRuntimeScriptingContext>(&gNoOpFactory); |
| auto vm = rive::make_rcp<rive::ScriptingVM>(std::move(context)); |
| |
| auto bytes = ReadFile("assets/data_global_repro.riv"); |
| rive::ImportResult result; |
| auto file = |
| rive::File::import(bytes, &gNoOpFactory, &result, nullptr, vm.get()); |
| REQUIRE(result == rive::ImportResult::success); |
| REQUIRE(file != nullptr); |
| |
| // The file adopts the VM we supplied... |
| REQUIRE(file->scriptingVM() == vm.get()); |
| |
| // ...and its `Data` global is a populated table (was nil before the fix), |
| // exposing the file's view model as a constructor. |
| lua_State* L = vm->state(); |
| REQUIRE(L != nullptr); |
| lua_getglobal(L, "Data"); |
| REQUIRE(lua_istable(L, -1)); |
| lua_getfield(L, -1, "ProbeChipVM"); |
| REQUIRE(lua_istable(L, -1)); |
| lua_getfield(L, -1, "new"); |
| CHECK(lua_isfunction(L, -1)); |
| lua_pop(L, 3); |
| } |
| |
| TEST_CASE("script has access to the data bound view model", "[silver]") |
| { |
| rive::SerializingFactory silver; |
| auto file = ReadRiveFile("assets/viewmodel_from_context.riv", &silver); |
| auto artboard = file->artboardNamed("main"); |
| |
| silver.frameSize(artboard->width(), artboard->height()); |
| REQUIRE(artboard != nullptr); |
| auto stateMachine = artboard->stateMachineAt(0); |
| int viewModelId = artboard.get()->viewModelId(); |
| |
| auto vmi = viewModelId == -1 |
| ? file->createViewModelInstance(artboard.get()) |
| : file->createViewModelInstance(viewModelId, 0); |
| stateMachine->bindViewModelInstance(vmi); |
| stateMachine->advanceAndApply(0.1f); |
| |
| auto renderer = silver.makeRenderer(); |
| artboard->draw(renderer.get()); |
| silver.addFrame(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| |
| CHECK(silver.matches("viewmodel_from_context")); |
| } |
| |
| TEST_CASE("script has access to the data root view model", "[silver]") |
| { |
| rive::SerializingFactory silver; |
| auto file = ReadRiveFile("assets/scripting_root_viewmodel.riv", &silver); |
| auto artboard = file->artboardNamed("parent"); |
| |
| silver.frameSize(artboard->width(), artboard->height()); |
| REQUIRE(artboard != nullptr); |
| auto stateMachine = artboard->stateMachineAt(0); |
| |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); |
| stateMachine->bindViewModelInstance(vmi); |
| stateMachine->advanceAndApply(0.1f); |
| |
| auto renderer = silver.makeRenderer(); |
| artboard->draw(renderer.get()); |
| silver.addFrame(); |
| stateMachine->advanceAndApply(0.1f); |
| artboard->draw(renderer.get()); |
| |
| CHECK(silver.matches("scripting_root_viewmodel")); |
| } |
| |
| TEST_CASE("context:image returns image asset by name", "[scripting]") |
| { |
| // Load a file with image assets using SerializingFactory which decodes |
| // images |
| rive::SerializingFactory silver; |
| auto file = ReadRiveFile("assets/walle.riv", &silver); |
| REQUIRE(file != nullptr); |
| |
| // Verify the file has image assets with render images |
| auto assets = file->assets(); |
| bool foundImageWithRenderImage = false; |
| for (const auto& asset : assets) |
| { |
| if (asset->is<ImageAsset>()) |
| { |
| ImageAsset* imageAsset = asset->as<ImageAsset>(); |
| if (imageAsset->name() == "walle.jpg" && |
| imageAsset->renderImage() != nullptr) |
| { |
| foundImageWithRenderImage = true; |
| break; |
| } |
| } |
| } |
| REQUIRE(foundImageWithRenderImage); |
| |
| // Create a ScriptedObject with a ScriptAsset that references this file |
| ScriptedObjectWithFile scriptedObjectWithFile; |
| scriptedObjectWithFile.setFileForScriptAsset(file.get()); |
| |
| // Create a scripting VM and test context:image() |
| ScriptingTest vm( |
| R"( |
| local foundImage = nil |
| local imageWidth = 0 |
| local imageHeight = 0 |
| |
| function testImage(context: Context) |
| local img = context:image("walle.jpg") |
| if img then |
| foundImage = true |
| imageWidth = img.width |
| imageHeight = img.height |
| else |
| foundImage = false |
| end |
| end |
| |
| function getFoundImage(): boolean? |
| return foundImage |
| end |
| |
| function getImageWidth(): number |
| return imageWidth |
| end |
| |
| function getImageHeight(): number |
| return imageHeight |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| // Call testImage with the context |
| { |
| lua_getglobal(L, "testImage"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectWithFile); |
| int result = lua_pcall(L, 1, 0, 0); |
| if (result != LUA_OK) |
| { |
| const char* error = lua_tostring(L, -1); |
| fprintf(stderr, "Lua error: %s\n", error); |
| lua_pop(L, 1); |
| } |
| CHECK(result == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| // Verify the image was found |
| { |
| lua_getglobal(L, "getFoundImage"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(lua_toboolean(L, -1) == 1); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| // Verify dimensions (SerializingFactory provides real decoded images) |
| { |
| lua_getglobal(L, "getImageWidth"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(lua_tonumber(L, -1) > 0); |
| lua_pop(L, 1); |
| |
| lua_getglobal(L, "getImageHeight"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(lua_tonumber(L, -1) > 0); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:image returns nil for non-existent image", "[scripting]") |
| { |
| // Load a file with image assets |
| rive::SerializingFactory silver; |
| auto file = ReadRiveFile("assets/walle.riv", &silver); |
| REQUIRE(file != nullptr); |
| |
| // Create a ScriptedObject with a ScriptAsset that references this file |
| ScriptedObjectWithFile scriptedObjectWithFile; |
| scriptedObjectWithFile.setFileForScriptAsset(file.get()); |
| |
| // Create a scripting VM and test context:image() with non-existent name |
| ScriptingTest vm( |
| R"( |
| local foundImage = nil |
| |
| function testNonExistentImage(context: Context) |
| local img = context:image("this_image_does_not_exist") |
| if img then |
| foundImage = true |
| else |
| foundImage = false |
| end |
| end |
| |
| function getFoundImage(): boolean? |
| return foundImage |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| // Call testNonExistentImage with the context |
| { |
| lua_getglobal(L, "testNonExistentImage"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectWithFile); |
| int result = lua_pcall(L, 1, 0, 0); |
| if (result != LUA_OK) |
| { |
| const char* error = lua_tostring(L, -1); |
| fprintf(stderr, "Lua error: %s\n", error); |
| lua_pop(L, 1); |
| } |
| CHECK(result == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| // Verify the image was NOT found (should be false, not nil) |
| { |
| lua_getglobal(L, "getFoundImage"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:image returns nil when no script asset", "[scripting]") |
| { |
| // Create a ScriptedObject WITHOUT a file (default ScriptedObjectTest) |
| ScriptedObjectTest scriptedObjectTest; |
| |
| // Create a scripting VM and test context:image() |
| ScriptingTest vm( |
| R"( |
| local foundImage = nil |
| |
| function testNoFile(context: Context) |
| local img = context:image("anyname") |
| if img then |
| foundImage = true |
| else |
| foundImage = false |
| end |
| end |
| |
| function getFoundImage(): boolean? |
| return foundImage |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| // Call testNoFile with the context (which has no scriptAsset/file) |
| { |
| lua_getglobal(L, "testNoFile"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| int result = lua_pcall(L, 1, 0, 0); |
| if (result != LUA_OK) |
| { |
| const char* error = lua_tostring(L, -1); |
| fprintf(stderr, "Lua error: %s\n", error); |
| lua_pop(L, 1); |
| } |
| CHECK(result == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| // Verify the image was NOT found |
| { |
| lua_getglobal(L, "getFoundImage"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("expose data context to scripts through context", "[silver]") |
| { |
| SerializingFactory silver; |
| auto file = ReadRiveFile("assets/scripted_data_context.riv", &silver); |
| |
| auto artboard = file->artboardNamed("Main"); |
| silver.frameSize(artboard->width(), artboard->height()); |
| |
| auto stateMachine = artboard->stateMachineAt(0); |
| |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); |
| stateMachine->bindViewModelInstance(vmi); |
| auto renderer = silver.makeRenderer(); |
| stateMachine->advanceAndApply(0.016f); |
| artboard->draw(renderer.get()); |
| |
| CHECK(silver.matches("scripted_data_context")); |
| } |
| |
| TEST_CASE("Provide data context and view model instance to artboard", |
| "[silver]") |
| { |
| SerializingFactory silver; |
| auto file = |
| ReadRiveFile("assets/viewmodel_instance_to_artboard.riv", &silver); |
| |
| auto artboard = file->artboardDefault(); |
| silver.frameSize(artboard->width(), artboard->height()); |
| |
| auto stateMachine = artboard->stateMachineAt(0); |
| |
| auto vmi = file->createDefaultViewModelInstance(artboard.get()); |
| |
| stateMachine->bindViewModelInstance(vmi); |
| auto renderer = silver.makeRenderer(); |
| stateMachine->advanceAndApply(0.016f); |
| artboard->draw(renderer.get()); |
| |
| int frames = (int)(1.0f / 0.016f); |
| for (int i = 0; i < frames; i++) |
| { |
| silver.addFrame(); |
| stateMachine->advanceAndApply(0.016f); |
| artboard->draw(renderer.get()); |
| } |
| |
| CHECK(silver.matches("viewmodel_instance_to_artboard")); |
| } |
| |
| TEST_CASE("context methods error on disposed context", "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| function testDisposed(context: Context) |
| context:markNeedsUpdate() |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| // Create a context and dispose it before calling methods |
| auto* ctx = lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| int ctxIdx = lua_gettop(L); |
| ctx->clearScriptedObject(); |
| CHECK(ctx->scriptedObject() == nullptr); |
| |
| // Calling a method on a disposed context should error |
| { |
| lua_getglobal(L, "testDisposed"); |
| lua_pushvalue(L, ctxIdx); |
| int result = lua_pcall(L, 1, 0, 0); |
| CHECK(result == LUA_ERRRUN); |
| // Error message should mention "disposed context" |
| const char* err = lua_tostring(L, -1); |
| CHECK(err != nullptr); |
| CHECK(std::string(err).find("disposed context") != std::string::npos); |
| lua_pop(L, 1); // pop error |
| } |
| |
| lua_pop(L, 1); // pop ctx |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| TEST_CASE("context:viewModel returns nil with no data context", "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| local result = "not_called" |
| |
| function testViewModel(context: Context) |
| local vm = context:viewModel() |
| if vm == nil then |
| result = "nil" |
| else |
| result = "found" |
| end |
| end |
| |
| function getResult(): string |
| return result |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testViewModel"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| { |
| lua_getglobal(L, "getResult"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(std::string(lua_tostring(L, -1)) == "nil"); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:rootViewModel returns nil with no data context", |
| "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| local result = "not_called" |
| |
| function testRootViewModel(context: Context) |
| local vm = context:rootViewModel() |
| if vm == nil then |
| result = "nil" |
| else |
| result = "found" |
| end |
| end |
| |
| function getResult(): string |
| return result |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testRootViewModel"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| { |
| lua_getglobal(L, "getResult"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(std::string(lua_tostring(L, -1)) == "nil"); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:dataContext returns nil with no data context", "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| local result = "not_called" |
| |
| function testDataContext(context: Context) |
| local dc = context:dataContext() |
| if dc == nil then |
| result = "nil" |
| else |
| result = "found" |
| end |
| end |
| |
| function getResult(): string |
| return result |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testDataContext"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| { |
| lua_getglobal(L, "getResult"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(std::string(lua_tostring(L, -1)) == "nil"); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:features returns fallback table without RIVE_CANVAS", |
| "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| local featuresTable = nil |
| |
| function testFeatures(context: Context) |
| featuresTable = context:features() |
| end |
| |
| function getFeatures() |
| return featuresTable |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testFeatures"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| // Verify features table was returned with expected fallback values |
| { |
| lua_getglobal(L, "getFeatures"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(lua_istable(L, -1)); |
| |
| // Check boolean fields default to false |
| lua_getfield(L, -1, "bc"); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "etc2"); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "astc"); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "anisotropicFiltering"); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "texture3D"); |
| CHECK(lua_toboolean(L, -1) == 0); |
| lua_pop(L, 1); |
| |
| // Check numeric fields have expected defaults |
| lua_getfield(L, -1, "maxTextureSize2D"); |
| CHECK(lua_tonumber(L, -1) == 4096); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "maxTextureSizeCube"); |
| CHECK(lua_tonumber(L, -1) == 4096); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "maxTextureSize3D"); |
| CHECK(lua_tonumber(L, -1) == 256); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "maxColorAttachments"); |
| CHECK(lua_tonumber(L, -1) == 4); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "maxUniformBufferSize"); |
| CHECK(lua_tonumber(L, -1) == 16384); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "maxSamplers"); |
| CHECK(lua_tonumber(L, -1) == 16); |
| lua_pop(L, 1); |
| |
| lua_getfield(L, -1, "maxSamples"); |
| CHECK(lua_tonumber(L, -1) == 4); |
| lua_pop(L, 1); |
| |
| lua_pop(L, 1); // pop table |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:preferredCanvasFormat is removed", "[scripting]") |
| { |
| // Removed in favor of canvas.format, which reports the speculative format |
| // even before a deferred canvas is resized. |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| function testRemoved(context: Context) |
| context:preferredCanvasFormat() |
| end |
| )", |
| 1, |
| true); // errorOk = true |
| |
| lua_State* L = vm.state(); |
| |
| lua_getglobal(L, "testRemoved"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| int result = lua_pcall(L, 1, 0, 0); |
| CHECK(result != LUA_OK); |
| const char* error = lua_tostring(L, -1); |
| CHECK(std::string(error).find("is not a valid method") != |
| std::string::npos); |
| lua_pop(L, 1); |
| } |
| |
| TEST_CASE("context invalid method raises error", "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| function testInvalidMethod(context: Context) |
| context:thisMethodDoesNotExist() |
| end |
| )", |
| 1, |
| true); // errorOk = true |
| |
| lua_State* L = vm.state(); |
| |
| lua_getglobal(L, "testInvalidMethod"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| int result = lua_pcall(L, 1, 0, 0); |
| CHECK(result != LUA_OK); |
| // Error message should mention invalid method |
| const char* error = lua_tostring(L, -1); |
| CHECK(std::string(error).find("is not a valid method") != |
| std::string::npos); |
| lua_pop(L, 1); |
| } |
| |
| TEST_CASE("context:blob returns nil when no script asset", "[scripting]") |
| { |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| local result = "not_called" |
| |
| function testBlob(context: Context) |
| local b = context:blob("anyname") |
| if b == nil then |
| result = "nil" |
| else |
| result = "found" |
| end |
| end |
| |
| function getResult(): string |
| return result |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testBlob"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| { |
| lua_getglobal(L, "getResult"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(std::string(lua_tostring(L, -1)) == "nil"); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:blob returns nil for non-existent blob", "[scripting]") |
| { |
| rive::SerializingFactory silver; |
| auto file = ReadRiveFile("assets/walle.riv", &silver); |
| REQUIRE(file != nullptr); |
| |
| ScriptedObjectWithFile scriptedObjectWithFile; |
| scriptedObjectWithFile.setFileForScriptAsset(file.get()); |
| |
| ScriptingTest vm( |
| R"( |
| local result = "not_called" |
| |
| function testBlob(context: Context) |
| local b = context:blob("nonexistent_blob") |
| if b == nil then |
| result = "nil" |
| else |
| result = "found" |
| end |
| end |
| |
| function getResult(): string |
| return result |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testBlob"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectWithFile); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| } |
| |
| { |
| lua_getglobal(L, "getResult"); |
| CHECK(lua_pcall(L, 0, 1, 0) == LUA_OK); |
| CHECK(std::string(lua_tostring(L, -1)) == "nil"); |
| lua_pop(L, 1); |
| CHECK(top == lua_gettop(L)); |
| } |
| } |
| |
| TEST_CASE("context:markNeedsUpdate on context without dataContext", |
| "[scripting]") |
| { |
| // markNeedsUpdate should work regardless of dataContext |
| ScriptedObjectTest scriptedObjectTest; |
| |
| ScriptingTest vm( |
| R"( |
| function testMark(context: Context) |
| context:markNeedsUpdate() |
| end |
| )"); |
| |
| lua_State* L = vm.state(); |
| auto top = lua_gettop(L); |
| |
| { |
| lua_getglobal(L, "testMark"); |
| lua_newrive<ScriptedContext>(L, &scriptedObjectTest); |
| CHECK(lua_pcall(L, 1, 0, 0) == LUA_OK); |
| CHECK(top == lua_gettop(L)); |
| CHECK(scriptedObjectTest.needsUpdate()); |
| } |
| } |
| |
| TEST_CASE("ScriptingContext ore/render context default to null", "[scripting]") |
| { |
| // Guard against regression where ore/render context were static globals. |
| // A freshly created VM must start with null ore and render context — not |
| // inheriting any previously set value from another VM or a prior run. |
| ScriptingTest vm("-- empty"); |
| ScriptingContext* ctx = vm.vm()->context(); |
| REQUIRE(ctx != nullptr); |
| CHECK(ctx->oreContext() == nullptr); |
| CHECK(ctx->renderContext() == nullptr); |
| } |