diff --git a/include/scratchcpp/textbubble.h b/include/scratchcpp/textbubble.h index baec89454..890b2b55b 100644 --- a/include/scratchcpp/textbubble.h +++ b/include/scratchcpp/textbubble.h @@ -8,6 +8,7 @@ namespace libscratchcpp { +class Thread; class TextBubblePrivate; /*! \brief The TextBubble class represents a text bubble created using say or think block. */ @@ -31,8 +32,11 @@ class LIBSCRATCHCPP_EXPORT TextBubble : public Drawable const std::string &text() const; virtual void setText(const std::string &text); + virtual void setText(const std::string &text, Thread *owner); sigslot::signal &textChanged() const; + Thread *owner() const; + private: spimpl::unique_impl_ptr impl; }; diff --git a/src/blocks/looksblocks.cpp b/src/blocks/looksblocks.cpp index 8da59e7a9..ae4ab08fc 100644 --- a/src/blocks/looksblocks.cpp +++ b/src/blocks/looksblocks.cpp @@ -1,5 +1,25 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "looksblocks.h" using namespace libscratchcpp; @@ -21,4 +41,558 @@ Rgb LooksBlocks::color() const void LooksBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "looks_sayforsecs", &compileSayForSecs); + engine->addCompileFunction(this, "looks_say", &compileSay); + engine->addCompileFunction(this, "looks_thinkforsecs", &compileThinkForSecs); + engine->addCompileFunction(this, "looks_think", &compileThink); + engine->addCompileFunction(this, "looks_show", &compileShow); + engine->addCompileFunction(this, "looks_hide", &compileHide); + engine->addCompileFunction(this, "looks_changeeffectby", &compileChangeEffectBy); + engine->addCompileFunction(this, "looks_seteffectto", &compileSetEffectTo); + engine->addCompileFunction(this, "looks_cleargraphiceffects", &compileClearGraphicEffects); + engine->addCompileFunction(this, "looks_changesizeby", &compileChangeSizeBy); + engine->addCompileFunction(this, "looks_setsizeto", &compileSetSizeTo); + engine->addCompileFunction(this, "looks_size", &compileSize); + engine->addCompileFunction(this, "looks_switchcostumeto", &compileSwitchCostumeTo); + engine->addCompileFunction(this, "looks_nextcostume", &compileNextCostume); + engine->addCompileFunction(this, "looks_switchbackdropto", &compileSwitchBackdropTo); + engine->addCompileFunction(this, "looks_gotofrontback", &compileGoToFrontBack); + engine->addCompileFunction(this, "looks_goforwardbackwardlayers", &compileGoForwardBackwardLayers); + engine->addCompileFunction(this, "looks_backdropnumbername", &compileBackdropNumberName); + engine->addCompileFunction(this, "looks_costumenumbername", &compileCostumeNumberName); + engine->addCompileFunction(this, "looks_switchbackdroptoandwait", &compileSwitchBackdropToAndWait); + engine->addCompileFunction(this, "looks_nextbackdrop", &compileNextBackdrop); +} + +void LooksBlocks::onInit(IEngine *engine) +{ + engine->threadAboutToStop().connect([](Thread *thread) { + /* + * TODO: Scratch uses promises for text bubble timeout + * which clear the text bubbles even after the thread + * destroyed. Since we don't use promises, we just + * clear the text bubble when the thread is destroyed. + */ + Target *target = thread->target(); + + if (target->bubble()->owner() == thread) + target->bubble()->setText(""); + }); + + engine->stopped().connect([engine]() { + const auto &targets = engine->targets(); + + for (auto target : targets) { + target->bubble()->setText(""); + target->clearGraphicsEffects(); + } + }); +} + +void LooksBlocks::compileSayOrThinkForSecs(Compiler *compiler, const std::string &function) +{ + auto message = compiler->addInput("MESSAGE"); + auto duration = compiler->addInput("SECS"); + auto saveThread = compiler->addConstValue(true); + compiler->addFunctionCallWithCtx(function, Compiler::StaticType::Void, { Compiler::StaticType::String, Compiler::StaticType::Bool }, { message, saveThread }); + compiler->addFunctionCallWithCtx("looks_start_stack_timer", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { duration }); + compiler->createYield(); + + compiler->beginLoopCondition(); + auto elapsed = compiler->addFunctionCallWithCtx("looks_update_bubble", Compiler::StaticType::Bool); + compiler->beginRepeatUntilLoop(elapsed); + compiler->endLoop(); +} + +void LooksBlocks::compileSetOrChangeEffect(Compiler *compiler, const std::string &function, const std::string &effectName, CompilerValue *arg) +{ + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect(effectName); + + if (effect) { + CompilerValue *effectPtr = compiler->addConstValue(effect); + compiler->addTargetFunctionCall(function, Compiler::StaticType::Void, { Compiler::StaticType::Pointer, Compiler::StaticType::Number }, { effectPtr, arg }); + } else + std::cout << "warning: graphic effect '" << effectName << "' is not registered" << std::endl; +} + +CompilerValue *LooksBlocks::compileSayForSecs(Compiler *compiler) +{ + compileSayOrThinkForSecs(compiler, "looks_say"); + return nullptr; +} + +CompilerValue *LooksBlocks::compileSay(Compiler *compiler) +{ + auto message = compiler->addInput("MESSAGE"); + auto saveThread = compiler->addConstValue(false); + compiler->addFunctionCallWithCtx("looks_say", Compiler::StaticType::Void, { Compiler::StaticType::String, Compiler::StaticType::Bool }, { message, saveThread }); + return nullptr; +} + +CompilerValue *LooksBlocks::compileThinkForSecs(Compiler *compiler) +{ + compileSayOrThinkForSecs(compiler, "looks_think"); + return nullptr; +} + +CompilerValue *LooksBlocks::compileThink(Compiler *compiler) +{ + auto message = compiler->addInput("MESSAGE"); + auto saveThread = compiler->addConstValue(false); + compiler->addFunctionCallWithCtx("looks_think", Compiler::StaticType::Void, { Compiler::StaticType::String, Compiler::StaticType::Bool }, { message, saveThread }); + return nullptr; +} + +CompilerValue *LooksBlocks::compileShow(Compiler *compiler) +{ + if (!compiler->target()->isStage()) + compiler->addTargetFunctionCall("looks_show"); + + return nullptr; +} + +CompilerValue *LooksBlocks::compileHide(Compiler *compiler) +{ + if (!compiler->target()->isStage()) + compiler->addTargetFunctionCall("looks_hide"); + + return nullptr; +} + +CompilerValue *LooksBlocks::compileChangeEffectBy(Compiler *compiler) +{ + Field *field = compiler->field("EFFECT"); + + if (!field) + return nullptr; + + CompilerValue *change = compiler->addInput("CHANGE"); + compileSetOrChangeEffect(compiler, "looks_changeeffectby", field->value().toString(), change); + return nullptr; +} + +CompilerValue *LooksBlocks::compileSetEffectTo(Compiler *compiler) +{ + Field *field = compiler->field("EFFECT"); + + if (!field) + return nullptr; + + CompilerValue *change = compiler->addInput("VALUE"); + compileSetOrChangeEffect(compiler, "looks_seteffectto", field->value().toString(), change); + return nullptr; +} + +CompilerValue *LooksBlocks::compileClearGraphicEffects(Compiler *compiler) +{ + compiler->addTargetFunctionCall("looks_cleargraphiceffects"); + return nullptr; +} + +CompilerValue *LooksBlocks::compileChangeSizeBy(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + auto change = compiler->addInput("CHANGE"); + compiler->addTargetFunctionCall("looks_changesizeby", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { change }); + return nullptr; +} + +CompilerValue *LooksBlocks::compileSetSizeTo(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + auto size = compiler->addInput("SIZE"); + compiler->addTargetFunctionCall("looks_setsizeto", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { size }); + return nullptr; +} + +CompilerValue *LooksBlocks::compileSize(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return compiler->addConstValue(100); + else + return compiler->addTargetFunctionCall("looks_size", Compiler::StaticType::Number); +} + +CompilerValue *LooksBlocks::compileSwitchCostumeTo(Compiler *compiler) +{ + auto costume = compiler->addInput("COSTUME"); + compiler->addTargetFunctionCall("looks_switchcostumeto", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { costume }); + return nullptr; +} + +CompilerValue *LooksBlocks::compileNextCostume(Compiler *compiler) +{ + compiler->addTargetFunctionCall("looks_nextcostume"); + return nullptr; +} + +CompilerValue *LooksBlocks::compileSwitchBackdropTo(Compiler *compiler) +{ + auto backdrop = compiler->addInput("BACKDROP"); + auto wait = compiler->addConstValue(false); + compiler->addFunctionCallWithCtx("looks_switchbackdropto", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { backdrop }); + compiler->addFunctionCallWithCtx("looks_start_backdrop_scripts", Compiler::StaticType::Void, { Compiler::StaticType::Bool }, { wait }); + + return nullptr; +} + +CompilerValue *LooksBlocks::compileGoToFrontBack(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + Field *field = compiler->field("FRONT_BACK"); + + if (!field) + return nullptr; + + const std::string &option = field->value().toString(); + + if (option == "front") + compiler->addFunctionCallWithCtx("looks_move_to_front"); + else if (option == "back") + compiler->addFunctionCallWithCtx("looks_move_to_back"); + + return nullptr; +} + +CompilerValue *LooksBlocks::compileGoForwardBackwardLayers(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + Field *field = compiler->field("FORWARD_BACKWARD"); + + if (!field) + return nullptr; + + const std::string &option = field->value().toString(); + + if (option == "forward") { + auto layers = compiler->addInput("NUM"); + compiler->addFunctionCallWithCtx("looks_move_forward_layers", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { layers }); + } else if (option == "backward") { + auto layers = compiler->addInput("NUM"); + compiler->addFunctionCallWithCtx("looks_move_backward_layers", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { layers }); + } + + return nullptr; +} + +CompilerValue *LooksBlocks::compileBackdropNumberName(Compiler *compiler) +{ + Field *field = compiler->field("NUMBER_NAME"); + + if (!field) + return nullptr; + + const std::string &option = field->value().toString(); + + if (option == "number") + return compiler->addFunctionCallWithCtx("looks_backdrop_number", Compiler::StaticType::Number); + else if (option == "name") + return compiler->addFunctionCallWithCtx("looks_backdrop_name", Compiler::StaticType::String); + else + return compiler->addConstValue(Value()); +} + +CompilerValue *LooksBlocks::compileCostumeNumberName(Compiler *compiler) +{ + Field *field = compiler->field("NUMBER_NAME"); + + if (!field) + return nullptr; + + const std::string &option = field->value().toString(); + + if (option == "number") + return compiler->addTargetFunctionCall("looks_costume_number", Compiler::StaticType::Number); + else if (option == "name") + return compiler->addTargetFunctionCall("looks_costume_name", Compiler::StaticType::String); + else + return compiler->addConstValue(Value()); +} + +CompilerValue *LooksBlocks::compileSwitchBackdropToAndWait(Compiler *compiler) +{ + auto backdrop = compiler->addInput("BACKDROP"); + auto wait = compiler->addConstValue(true); + compiler->addFunctionCallWithCtx("looks_switchbackdropto", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { backdrop }); + compiler->addFunctionCallWithCtx("looks_start_backdrop_scripts", Compiler::StaticType::Void, { Compiler::StaticType::Bool }, { wait }); + + auto hasBackdrops = compiler->addFunctionCallWithCtx("looks_backdrop_promise", Compiler::StaticType::Bool); + compiler->beginIfStatement(hasBackdrops); + compiler->createYield(); + compiler->endIf(); + + return nullptr; +} + +CompilerValue *LooksBlocks::compileNextBackdrop(Compiler *compiler) +{ + compiler->addFunctionCallWithCtx("looks_nextbackdrop"); + return nullptr; +} + +extern "C" void looks_start_stack_timer(ExecutionContext *ctx, double duration) +{ + ctx->stackTimer()->start(duration); +} + +void looks_show_bubble(Thread *thread, TextBubble::Type type, const StringPtr *message, bool saveThread) +{ + Target *target = thread->target(); + // TODO: Use UTF-16 for text bubbles + std::string u8message = utf8::utf16to8(std::u16string(message->data)); + target->bubble()->setType(type); + + if (saveThread) + target->bubble()->setText(u8message, thread); + else + target->bubble()->setText(u8message); +} + +extern "C" bool looks_update_bubble(ExecutionContext *ctx) +{ + if (ctx->stackTimer()->elapsed()) { + Thread *thread = ctx->thread(); + Target *target = thread->target(); + + // Clear bubble if it hasn't been changed + if (target->bubble()->owner() == thread) + target->bubble()->setText(""); + + return true; + } + + return false; +} + +extern "C" void looks_say(ExecutionContext *ctx, const StringPtr *message, bool saveThread) +{ + looks_show_bubble(ctx->thread(), TextBubble::Type::Say, message, saveThread); +} + +extern "C" void looks_think(ExecutionContext *ctx, const StringPtr *message, bool saveThread) +{ + looks_show_bubble(ctx->thread(), TextBubble::Type::Think, message, saveThread); +} + +extern "C" void looks_show(Sprite *sprite) +{ + sprite->setVisible(true); +} + +extern "C" void looks_hide(Sprite *sprite) +{ + sprite->setVisible(false); +} + +extern "C" void looks_changeeffectby(Target *target, IGraphicsEffect *effect, double change) +{ + target->setGraphicsEffectValue(effect, target->graphicsEffectValue(effect) + change); +} + +extern "C" void looks_seteffectto(Target *target, IGraphicsEffect *effect, double value) +{ + target->setGraphicsEffectValue(effect, value); +} + +extern "C" void looks_cleargraphiceffects(Target *target) +{ + target->clearGraphicsEffects(); +} + +extern "C" void looks_changesizeby(Sprite *sprite, double change) +{ + sprite->setSize(sprite->size() + change); +} + +extern "C" void looks_setsizeto(Sprite *sprite, double size) +{ + sprite->setSize(size); +} + +extern "C" double looks_size(Sprite *sprite) +{ + return sprite->size(); +} + +extern "C" void looks_set_costume_by_index(Target *target, long index) +{ + const size_t costumeCount = target->costumes().size(); + + if (index < 0) + index = (costumeCount + index % (-costumeCount)) % costumeCount; + else if (index >= costumeCount) + index = index % costumeCount; + + target->setCostumeIndex(index); +} + +extern "C" void looks_nextcostume(Target *target) +{ + looks_set_costume_by_index(target, target->costumeIndex() + 1); +} + +extern "C" void looks_previouscostume(Target *target) +{ + looks_set_costume_by_index(target, target->costumeIndex() - 1); +} + +void looks_randomcostume(Target *target, IRandomGenerator *rng) +{ + size_t count = target->costumes().size(); + + if (count > 1) + looks_set_costume_by_index(target, rng->randintExcept(0, count - 1, target->costumeIndex())); // exclude current costume +} + +extern "C" void looks_switchcostumeto(Target *target, const ValueData *costume) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/blocks/scratch3_looks.js#L389-L413 + if (!value_isString(costume)) { + // Numbers should be treated as costume indices, always + if (value_isNaN(costume) || value_isInfinity(costume) || value_isNegativeInfinity(costume)) + target->setCostumeIndex(0); + else + looks_set_costume_by_index(target, value_toLong(costume) - 1); + } else { + // Strings should be treated as costume names, where possible + // TODO: Use UTF-16 in Target + // StringPtr *nameStr = value_toStringPtr(costume); + std::string nameStr; + value_toString(costume, &nameStr); + const int costumeIndex = target->findCostume(nameStr); + + auto it = std::find_if(nameStr.begin(), nameStr.end(), [](char c) { return !std::isspace(c); }); + bool isWhiteSpace = (it == nameStr.end()); + + if (costumeIndex != -1) + looks_set_costume_by_index(target, costumeIndex); + else if (nameStr == "next costume") + looks_nextcostume(target); + else if (nameStr == "previous costume") { + looks_previouscostume(target); + // Try to cast the string to a number (and treat it as a costume index) + // Pure whitespace should not be treated as a number + } else if (value_isValidNumber(costume) && !isWhiteSpace) + looks_set_costume_by_index(target, value_toLong(costume) - 1); + } +} + +extern "C" void looks_start_backdrop_scripts(ExecutionContext *ctx, bool wait) +{ + IEngine *engine = ctx->engine(); + Stage *stage = engine->stage(); + Costume *backdrop = stage->currentCostume().get(); + + if (backdrop) + engine->startBackdropScripts(backdrop->broadcast(), ctx->thread(), wait); +} + +extern "C" void looks_switchbackdropto(ExecutionContext *ctx, const ValueData *backdrop) +{ + Stage *stage = ctx->engine()->stage(); + + // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/blocks/scratch3_looks.js#L389-L413 + if (!value_isString(backdrop)) { + // Numbers should be treated as costume indices, always + if (value_isNaN(backdrop) || value_isInfinity(backdrop) || value_isNegativeInfinity(backdrop)) + stage->setCostumeIndex(0); + else + looks_set_costume_by_index(stage, value_toLong(backdrop) - 1); + } else { + // Strings should be treated as costume names, where possible + // TODO: Use UTF-16 in Target + // StringPtr *nameStr = value_toStringPtr(backdrop); + std::string nameStr; + value_toString(backdrop, &nameStr); + const int costumeIndex = stage->findCostume(nameStr); + + auto it = std::find_if(nameStr.begin(), nameStr.end(), [](char c) { return !std::isspace(c); }); + bool isWhiteSpace = (it == nameStr.end()); + + if (costumeIndex != -1) + looks_set_costume_by_index(stage, costumeIndex); + else if (nameStr == "next backdrop") + looks_nextcostume(stage); + else if (nameStr == "previous backdrop") + looks_previouscostume(stage); + else if (nameStr == "random backdrop") { + looks_randomcostume(stage, ctx->rng()); + // Try to cast the string to a number (and treat it as a costume index) + // Pure whitespace should not be treated as a number + } else if (value_isValidNumber(backdrop) && !isWhiteSpace) + looks_set_costume_by_index(stage, value_toLong(backdrop) - 1); + } +} + +extern "C" void looks_move_to_front(ExecutionContext *ctx) +{ + Target *target = ctx->thread()->target(); + ctx->engine()->moveDrawableToFront(target); +} + +extern "C" void looks_move_to_back(ExecutionContext *ctx) +{ + Target *target = ctx->thread()->target(); + ctx->engine()->moveDrawableToBack(target); +} + +extern "C" void looks_move_forward_layers(ExecutionContext *ctx, double layers) +{ + Target *target = ctx->thread()->target(); + ctx->engine()->moveDrawableForwardLayers(target, layers); +} + +extern "C" void looks_move_backward_layers(ExecutionContext *ctx, double layers) +{ + Target *target = ctx->thread()->target(); + ctx->engine()->moveDrawableBackwardLayers(target, layers); +} + +extern "C" double looks_backdrop_number(ExecutionContext *ctx) +{ + return ctx->engine()->stage()->costumeIndex() + 1; +} + +extern "C" StringPtr *looks_backdrop_name(ExecutionContext *ctx) +{ + const std::string &name = ctx->engine()->stage()->currentCostume()->name(); + StringPtr *ret = string_pool_new(); + string_assign_cstring(ret, name.c_str()); + return ret; +} + +extern "C" double looks_costume_number(Target *target) +{ + return target->costumeIndex() + 1; +} + +extern "C" StringPtr *looks_costume_name(Target *target) +{ + const std::string &name = target->currentCostume()->name(); + StringPtr *ret = string_pool_new(); + string_assign_cstring(ret, name.c_str()); + return ret; +} + +extern "C" bool looks_backdrop_promise(ExecutionContext *ctx) +{ + if (ctx->engine()->stage()->costumes().size() > 0) { + ctx->setPromise(std::make_shared()); + return true; + } + + return false; +} + +extern "C" void looks_nextbackdrop(ExecutionContext *ctx) +{ + looks_nextcostume(ctx->engine()->stage()); } diff --git a/src/blocks/looksblocks.h b/src/blocks/looksblocks.h index b75297549..334202346 100644 --- a/src/blocks/looksblocks.h +++ b/src/blocks/looksblocks.h @@ -3,10 +3,16 @@ #pragma once #include +#include +#include namespace libscratchcpp { +class Target; +class Thread; +class IGraphicsEffect; + class LooksBlocks : public IExtension { public: @@ -15,6 +21,33 @@ class LooksBlocks : public IExtension Rgb color() const override; void registerBlocks(IEngine *engine) override; + void onInit(IEngine *engine) override; + + private: + static void compileSayOrThinkForSecs(Compiler *compiler, const std::string &function); + static void compileSetOrChangeEffect(Compiler *compiler, const std::string &function, const std::string &effectName, CompilerValue *arg); + + static CompilerValue *compileSayForSecs(Compiler *compiler); + static CompilerValue *compileSay(Compiler *compiler); + static CompilerValue *compileThinkForSecs(Compiler *compiler); + static CompilerValue *compileThink(Compiler *compiler); + static CompilerValue *compileShow(Compiler *compiler); + static CompilerValue *compileHide(Compiler *compiler); + static CompilerValue *compileChangeEffectBy(Compiler *compiler); + static CompilerValue *compileSetEffectTo(Compiler *compiler); + static CompilerValue *compileClearGraphicEffects(Compiler *compiler); + static CompilerValue *compileChangeSizeBy(Compiler *compiler); + static CompilerValue *compileSetSizeTo(Compiler *compiler); + static CompilerValue *compileSize(Compiler *compiler); + static CompilerValue *compileSwitchCostumeTo(Compiler *compiler); + static CompilerValue *compileNextCostume(Compiler *compiler); + static CompilerValue *compileSwitchBackdropTo(Compiler *compiler); + static CompilerValue *compileGoToFrontBack(Compiler *compiler); + static CompilerValue *compileGoForwardBackwardLayers(Compiler *compiler); + static CompilerValue *compileBackdropNumberName(Compiler *compiler); + static CompilerValue *compileCostumeNumberName(Compiler *compiler); + static CompilerValue *compileSwitchBackdropToAndWait(Compiler *compiler); + static CompilerValue *compileNextBackdrop(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/src/engine/internal/engine.cpp b/src/engine/internal/engine.cpp index 265e7e587..98612f577 100644 --- a/src/engine/internal/engine.cpp +++ b/src/engine/internal/engine.cpp @@ -321,17 +321,13 @@ void Engine::stop() // https://github.com/scratchfoundation/scratch-vm/blob/f1aa92fad79af17d9dd1c41eeeadca099339a9f1/src/engine/runtime.js#L2057-L2081 if (m_activeThread) { stopThread(m_activeThread.get()); - // NOTE: The project should continue running even after "stop all" is called and the remaining threads should be stepped once. - // The remaining threads can even start new threads which will ignore the "stop all" call and will "restart" the project. - // This is probably a bug in the Scratch VM, but let's keep it here to keep it compatible. - m_threadsToStop = m_threads; // Remove threads owned by clones because clones are going to be deleted (#547) m_threads.erase( std::remove_if( m_threads.begin(), m_threads.end(), - [](std::shared_ptr thread) { + [this](std::shared_ptr thread) { assert(thread); Target *target = thread->target(); assert(target); @@ -339,13 +335,20 @@ void Engine::stop() if (!target->isStage()) { Sprite *sprite = static_cast(target); - if (sprite->isClone()) + if (sprite->isClone()) { + m_threadAboutToStop(thread.get()); return true; + } } return false; }), m_threads.end()); + + // NOTE: The project should continue running even after "stop all" is called and the remaining threads should be stepped once. + // The remaining threads can even start new threads which will ignore the "stop all" call and will "restart" the project. + // This is probably a bug in the Scratch VM, but let's keep it here to keep it compatible. + m_threadsToStop = m_threads; } else { // If there isn't any active thread, it means the project was stopped from the outside // In this case all threads should be removed and the project should be considered stopped @@ -1259,16 +1262,26 @@ void Engine::moveDrawableForwardLayers(Drawable *drawable, int layers) if (it == m_sortedDrawables.end()) return; - auto target = it + layers; + auto target = it; + int layersAbs = std::abs(layers); - if (target <= m_sortedDrawables.begin()) { - moveDrawableToBack(drawable); - return; - } + for (int i = 0; i < layersAbs; i++) { + if (target <= m_sortedDrawables.begin()) { + moveDrawableToBack(drawable); + return; + } - if (target >= m_sortedDrawables.end()) { - moveDrawableToFront(drawable); - return; + if (target >= m_sortedDrawables.end()) { + moveDrawableToFront(drawable); + return; + } + + Drawable *currentDrawable; + + do { + currentDrawable = *target; + target += layers / layersAbs; + } while (currentDrawable->isTextBubble() && static_cast(currentDrawable)->text().empty()); } if (layers > 0) diff --git a/src/engine/internal/llvm/llvmcodebuilder.cpp b/src/engine/internal/llvm/llvmcodebuilder.cpp index 881633534..c08bba715 100644 --- a/src/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/engine/internal/llvm/llvmcodebuilder.cpp @@ -2256,6 +2256,9 @@ llvm::Value *LLVMCodeBuilder::castValue(LLVMRegister *reg, Compiler::StaticType return nullptr; } + case Compiler::StaticType::Unknown: + return createValue(reg); + default: assert(false); return nullptr; @@ -2320,6 +2323,9 @@ llvm::Value *LLVMCodeBuilder::castRawValue(LLVMRegister *reg, Compiler::StaticTy return nullptr; } + case Compiler::StaticType::Unknown: + return createValue(reg); + default: assert(false); return nullptr; diff --git a/src/scratch/textbubble.cpp b/src/scratch/textbubble.cpp index c27aece6d..0b78c5673 100644 --- a/src/scratch/textbubble.cpp +++ b/src/scratch/textbubble.cpp @@ -53,10 +53,16 @@ const std::string &TextBubble::text() const * \note If the text is an empty string, the TextBubble is supposed to be hidden. */ void TextBubble::setText(const std::string &text) +{ + setText(text, nullptr); +} + +void TextBubble::setText(const std::string &text, Thread *owner) { // https://github.com/scratchfoundation/scratch-vm/blob/7313ce5199f8a3da7850085d0f7f6a3ca2c89bf6/src/blocks/scratch3_looks.js#L251-L257 const Value v(text); std::string converted = text; + impl->owner = owner; // Non-integers should be rounded to 2 decimal places (no more, no less), unless they're small enough that rounding would display them as 0.00. if (v.isValidNumber()) { @@ -85,3 +91,9 @@ sigslot::signal &TextBubble::textChanged() const { return impl->textChanged; } + +/*! Returns the thread which currently owns the text bubble. */ +Thread *TextBubble::owner() const +{ + return impl->owner; +} diff --git a/src/scratch/textbubble_p.h b/src/scratch/textbubble_p.h index 118b4ebb8..4718b58c7 100644 --- a/src/scratch/textbubble_p.h +++ b/src/scratch/textbubble_p.h @@ -11,6 +11,7 @@ struct TextBubblePrivate { TextBubble::Type type = TextBubble::Type::Say; std::string text; + Thread *owner = nullptr; mutable sigslot::signal typeChanged; mutable sigslot::signal textChanged; }; diff --git a/test/blocks/looks_blocks_test.cpp b/test/blocks/looks_blocks_test.cpp index 9074d9d41..049217d18 100644 --- a/test/blocks/looks_blocks_test.cpp +++ b/test/blocks/looks_blocks_test.cpp @@ -1,15 +1,2878 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include "../common.h" #include "blocks/looksblocks.h" using namespace libscratchcpp; +using namespace libscratchcpp::test; + +using ::testing::Return; +using ::testing::ReturnArg; +using ::testing::_; class LooksBlocksTest : public testing::Test { public: - void SetUp() override { m_extension = std::make_unique(); } + void SetUp() override + { + m_extension = std::make_unique(); + m_engine = m_project.engine().get(); + m_extension->registerBlocks(m_engine); + m_extension->onInit(m_engine); + + // Create and register fake graphic effects + auto colorEffect = std::make_shared(); + auto fisheyeEffect = std::make_shared(); + auto whirlEffect = std::make_shared(); + auto pixelateEffect = std::make_shared(); + auto mosaicEffect = std::make_shared(); + auto brightnessEffect = std::make_shared(); + auto ghostEffect = std::make_shared(); + + EXPECT_CALL(*colorEffect, name()).WillOnce(Return("COLOR")); + ScratchConfiguration::registerGraphicsEffect(colorEffect); + + EXPECT_CALL(*fisheyeEffect, name()).WillOnce(Return("FISHEYE")); + ScratchConfiguration::registerGraphicsEffect(fisheyeEffect); + + EXPECT_CALL(*whirlEffect, name()).WillOnce(Return("WHIRL")); + ScratchConfiguration::registerGraphicsEffect(whirlEffect); + + EXPECT_CALL(*pixelateEffect, name()).WillOnce(Return("PIXELATE")); + ScratchConfiguration::registerGraphicsEffect(pixelateEffect); + + EXPECT_CALL(*mosaicEffect, name()).WillOnce(Return("MOSAIC")); + ScratchConfiguration::registerGraphicsEffect(mosaicEffect); + + EXPECT_CALL(*brightnessEffect, name()).WillOnce(Return("BRIGHTNESS")); + ScratchConfiguration::registerGraphicsEffect(brightnessEffect); + + EXPECT_CALL(*ghostEffect, name()).WillOnce(Return("GHOST")); + ScratchConfiguration::registerGraphicsEffect(ghostEffect); + + EXPECT_CALL(*colorEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(*fisheyeEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(*whirlEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(*pixelateEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(*mosaicEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(*brightnessEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + EXPECT_CALL(*ghostEffect, clamp(_)).WillRepeatedly(ReturnArg<0>()); + } + + void TearDown() override + { + // Remove fake graphic effects + ScratchConfiguration::removeGraphicsEffect("COLOR"); + ScratchConfiguration::removeGraphicsEffect("FISHEYE"); + ScratchConfiguration::removeGraphicsEffect("WHIRL"); + ScratchConfiguration::removeGraphicsEffect("PIXELATE"); + ScratchConfiguration::removeGraphicsEffect("MOSAIC"); + ScratchConfiguration::removeGraphicsEffect("BRIGHTNESS"); + ScratchConfiguration::removeGraphicsEffect("GHOST"); + } std::unique_ptr m_extension; + Project m_project; + IEngine *m_engine = nullptr; EngineMock m_engineMock; }; + +TEST_F(LooksBlocksTest, StopProject) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + GraphicsEffectMock effect; + m_engine->setTargets({ stage, sprite }); + + stage->bubble()->setText("abc"); + sprite->bubble()->setText("def"); + EXPECT_CALL(effect, clamp(10)).WillOnce(Return(10)); + sprite->setGraphicsEffectValue(&effect, 10); + EXPECT_CALL(effect, clamp(2.5)).WillOnce(Return(2.5)); + stage->setGraphicsEffectValue(&effect, 2.5); + + m_engine->stop(); + ASSERT_TRUE(stage->bubble()->text().empty()); + ASSERT_TRUE(sprite->bubble()->text().empty()); + ASSERT_EQ(stage->graphicsEffectValue(&effect), 0); + ASSERT_EQ(sprite->graphicsEffectValue(&effect), 0); +} + +TEST_F(LooksBlocksTest, SayAndThinkForSecs) +{ + std::vector types = { TextBubble::Type::Say, TextBubble::Type::Think }; + std::vector opcodes = { "looks_sayforsecs", "looks_thinkforsecs" }; + + for (int i = 0; i < types.size(); i++) { + TextBubble::Type type = types[i]; + const std::string &opcode = opcodes[i]; + + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + m_engine->clear(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock(opcode); + builder.addValueInput("MESSAGE", "Hello world"); + builder.addValueInput("SECS", 2.5); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->bubble()->text(), "Hello world"); + ASSERT_EQ(sprite->bubble()->type(), type); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(false)); + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->bubble()->text(), "Hello world"); + ASSERT_EQ(sprite->bubble()->type(), type); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_TRUE(sprite->bubble()->text().empty()); + + // Change text while waiting + code->reset(ctx.get()); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(m_engineMock, requestRedraw()); + sprite->bubble()->setText("test"); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->bubble()->text(), "test"); + ASSERT_EQ(sprite->bubble()->type(), type); + + code->reset(ctx.get()); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(m_engineMock, requestRedraw()); + sprite->bubble()->setText("Hello world"); + + EXPECT_CALL(timer, elapsed()).WillOnce(Return(true)); + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->bubble()->text(), "Hello world"); + ASSERT_EQ(sprite->bubble()->type(), type); + + // Kill waiting thread + code->reset(ctx.get()); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + m_engine->threadAboutToStop()(&thread); + code->kill(ctx.get()); + ASSERT_EQ(sprite->bubble()->owner(), nullptr); + ASSERT_TRUE(sprite->bubble()->text().empty()); + + // Kill waiting thread after changing text + code->reset(ctx.get()); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(m_engineMock, requestRedraw()); + sprite->bubble()->setText("test"); + + m_engine->threadAboutToStop()(&thread); + code->kill(ctx.get()); + ASSERT_EQ(sprite->bubble()->owner(), nullptr); + ASSERT_EQ(sprite->bubble()->text(), "test"); + ASSERT_EQ(sprite->bubble()->type(), type); + } +} + +TEST_F(LooksBlocksTest, SayAndThink) +{ + std::vector types = { TextBubble::Type::Say, TextBubble::Type::Think }; + std::vector opcodes = { "looks_say", "looks_think" }; + + for (int i = 0; i < types.size(); i++) { + TextBubble::Type type = types[i]; + const std::string &opcode = opcodes[i]; + + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + m_engine->clear(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock(opcode); + builder.addValueInput("MESSAGE", "Hello world"); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->bubble()->text(), "Hello world"); + ASSERT_EQ(sprite->bubble()->type(), type); + ASSERT_EQ(sprite->bubble()->owner(), nullptr); + } +} + +TEST_F(LooksBlocksTest, Show_Sprite) +{ + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_show"); + builder.build(); + + sprite->setVisible(false); + + builder.run(); + ASSERT_TRUE(sprite->visible()); + + builder.run(); + ASSERT_TRUE(sprite->visible()); +} + +TEST_F(LooksBlocksTest, Show_Stage) +{ + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_show"); + + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, Hide_Sprite) +{ + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_hide"); + builder.build(); + + sprite->setVisible(true); + + builder.run(); + ASSERT_FALSE(sprite->visible()); + + builder.run(); + ASSERT_FALSE(sprite->visible()); +} + +TEST_F(LooksBlocksTest, Hide_Stage) +{ + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_hide"); + + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, ChangeEffectBy_Color) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect("COLOR"); + ASSERT_TRUE(effect); + + builder.addBlock("looks_changeeffectby"); + builder.addDropdownField("EFFECT", "COLOR"); + builder.addValueInput("CHANGE", 45.2); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, m_engine); + script.setCode(code); + Thread thread(stage.get(), m_engine, &script); + + stage->setGraphicsEffectValue(effect, 86.84); + thread.run(); + ASSERT_EQ(std::round(stage->graphicsEffectValue(effect) * 100) / 100, 132.04); +} + +TEST_F(LooksBlocksTest, ChangeEffectBy_Brightness) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect("BRIGHTNESS"); + ASSERT_TRUE(effect); + + builder.addBlock("looks_changeeffectby"); + builder.addDropdownField("EFFECT", "BRIGHTNESS"); + builder.addValueInput("CHANGE", 12.05); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, m_engine); + script.setCode(code); + Thread thread(stage.get(), m_engine, &script); + + thread.run(); + ASSERT_EQ(std::round(stage->graphicsEffectValue(effect) * 100) / 100, 12.05); +} + +TEST_F(LooksBlocksTest, ChangeEffectBy_Ghost) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect("GHOST"); + ASSERT_TRUE(effect); + + builder.addBlock("looks_changeeffectby"); + builder.addDropdownField("EFFECT", "GHOST"); + builder.addValueInput("CHANGE", -8.06); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, m_engine); + script.setCode(code); + Thread thread(stage.get(), m_engine, &script); + + stage->setGraphicsEffectValue(effect, 13.12); + thread.run(); + ASSERT_EQ(std::round(stage->graphicsEffectValue(effect) * 100) / 100, 5.06); +} + +TEST_F(LooksBlocksTest, ChangeEffectBy_Invalid) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("looks_changeeffectby"); + builder.addDropdownField("EFFECT", "INVALID"); + builder.addValueInput("CHANGE", 8.3); + + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, SetEffectTo_Fisheye) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect("FISHEYE"); + ASSERT_TRUE(effect); + + builder.addBlock("looks_seteffectto"); + builder.addDropdownField("EFFECT", "FISHEYE"); + builder.addValueInput("VALUE", 45.2); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, m_engine); + script.setCode(code); + Thread thread(stage.get(), m_engine, &script); + + stage->setGraphicsEffectValue(effect, 86.84); + thread.run(); + ASSERT_EQ(std::round(stage->graphicsEffectValue(effect) * 100) / 100, 45.2); +} + +TEST_F(LooksBlocksTest, SetEffectTo_Pixelate) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect("PIXELATE"); + ASSERT_TRUE(effect); + + builder.addBlock("looks_seteffectto"); + builder.addDropdownField("EFFECT", "PIXELATE"); + builder.addValueInput("VALUE", 12.05); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, m_engine); + script.setCode(code); + Thread thread(stage.get(), m_engine, &script); + + thread.run(); + ASSERT_EQ(std::round(stage->graphicsEffectValue(effect) * 100) / 100, 12.05); +} + +TEST_F(LooksBlocksTest, SetEffectTo_Mosaic) +{ + auto sprite = std::make_shared(); + sprite->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + IGraphicsEffect *effect = ScratchConfiguration::getGraphicsEffect("MOSAIC"); + ASSERT_TRUE(effect); + + builder.addBlock("looks_seteffectto"); + builder.addDropdownField("EFFECT", "MOSAIC"); + builder.addValueInput("VALUE", -8.06); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, m_engine); + script.setCode(code); + Thread thread(sprite.get(), m_engine, &script); + + sprite->setGraphicsEffectValue(effect, 13.12); + thread.run(); + ASSERT_EQ(std::round(sprite->graphicsEffectValue(effect) * 100) / 100, -8.06); +} + +TEST_F(LooksBlocksTest, SetEffectTo_Invalid) +{ + auto stage = std::make_shared(); + stage->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("looks_seteffectto"); + builder.addDropdownField("EFFECT", "INVALID"); + builder.addValueInput("VALUE", 8.3); + + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, ClearGraphicEffects) +{ + auto sprite = std::make_shared(); + sprite->setEngine(m_engine); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + IGraphicsEffect *effect1 = ScratchConfiguration::getGraphicsEffect("WHIRL"); + IGraphicsEffect *effect2 = ScratchConfiguration::getGraphicsEffect("GHOST"); + IGraphicsEffect *effect3 = ScratchConfiguration::getGraphicsEffect("MOSAIC"); + ASSERT_TRUE(effect1); + ASSERT_TRUE(effect2); + ASSERT_TRUE(effect3); + + builder.addBlock("looks_cleargraphiceffects"); + auto block = builder.currentBlock(); + + Compiler compiler(m_engine, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, m_engine); + script.setCode(code); + Thread thread(sprite.get(), m_engine, &script); + + sprite->setGraphicsEffectValue(effect1, 86.84); + sprite->setGraphicsEffectValue(effect2, -5.18); + sprite->setGraphicsEffectValue(effect3, 12.98); + thread.run(); + ASSERT_EQ(sprite->graphicsEffectValue(effect1), 0); + ASSERT_EQ(sprite->graphicsEffectValue(effect2), 0); + ASSERT_EQ(sprite->graphicsEffectValue(effect3), 0); +} + +TEST_F(LooksBlocksTest, ChangeSizeBy_Sprite) +{ + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("looks_changesizeby"); + builder.addValueInput("CHANGE", 2.5); + builder.build(); + + sprite->setEngine(nullptr); + sprite->setSize(45.62); + builder.run(); + ASSERT_EQ(sprite->size(), 48.12); +} + +TEST_F(LooksBlocksTest, ChangeSizeBy_Stage) +{ + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("looks_changesizeby"); + builder.addValueInput("CHANGE", 2.5); + + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, SetSizeTo_Sprite) +{ + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("looks_setsizeto"); + builder.addValueInput("SIZE", 2.5); + builder.build(); + + sprite->setEngine(nullptr); + sprite->setSize(45.62); + builder.run(); + ASSERT_EQ(sprite->size(), 2.5); +} + +TEST_F(LooksBlocksTest, SetSizeTo_Stage) +{ + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("looks_setsizeto"); + builder.addValueInput("SIZE", 2.5); + + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, Size_Sprite) +{ + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("looks_size"); + builder.captureBlockReturnValue(); + builder.build(); + + sprite->setEngine(nullptr); + sprite->setSize(45.62); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 45.62); +} + +TEST_F(LooksBlocksTest, Size_Stage) +{ + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + builder.addBlock("looks_size"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 100); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_NoCostumes) +{ + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "costume2"); + builder.build(); + builder.run(); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_CostumeName) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "costume2"); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_InvalidCostumeName) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "invalid"); + builder.build(); + + sprite->setCostumeIndex(1); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); + + sprite->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_NextCostume) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "next costume"); + builder.build(); + + sprite->setCostumeIndex(0); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 2); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_NextCostumeExists) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto nextCostume = std::make_shared("next costume", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(nextCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "next costume"); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_PreviousCostume) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "previous costume"); + builder.build(); + + sprite->setCostumeIndex(0); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 2); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_PreviousCostumeExists) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto previousCostume = std::make_shared("previous costume", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(previousCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "previous costume"); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_NumberIndex) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", 3); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_PositiveOutOfRangeNumberIndex) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", 5); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_NegativeOutOfRangeNumberIndex) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", -1); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_InvalidNumberIndex) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", std::numeric_limits::quiet_NaN()); + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", std::numeric_limits::infinity()); + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", -std::numeric_limits::infinity()); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); + + sprite->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_StringIndex) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", "3"); + builder.build(); + + sprite->setCostumeIndex(1); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_StringIndexExists) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", "3"); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_OutOfRangeStringIndex) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", "+7.0"); + builder.build(); + + sprite->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_WhitespaceString) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto numberCostume = std::make_shared("3", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(numberCostume); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", ""); + builder.addBlock("looks_switchcostumeto"); + builder.addValueInput("COSTUME", " "); + builder.build(); + + sprite->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchCostumeTo_Stage) +{ + auto stage = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + stage->addCostume(costume1); + stage->addCostume(costume2); + stage->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_switchcostumeto"); + builder.addDropdownInput("COSTUME", "costume1"); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, NextCostume_Sprite) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_nextcostume"); + builder.build(); + + sprite->setCostumeIndex(0); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 2); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, NextCostume_Stage) +{ + auto stage = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + stage->addCostume(costume1); + stage->addCostume(costume2); + stage->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_nextcostume"); + builder.build(); + + stage->setCostumeIndex(0); + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_NoBackdrops) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "backdrop2"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts).Times(0); + thread.run(); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_BackdropName) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "backdrop2"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_InvalidBackdropName) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "invalid"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, false)); + stage->setCostumeIndex(1); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, false)); + stage->setCostumeIndex(2); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_NextBackdrop) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "next backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, false)); + stage->setCostumeIndex(0); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_NextBackdropExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto nextBackdrop = std::make_shared("next backdrop", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(nextBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "next backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(nextBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(nextBackdrop->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_PreviousBackdrop) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "previous backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, false)); + stage->setCostumeIndex(0); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_PreviousBackdropExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto previousBackdrop = std::make_shared("previous backdrop", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(previousBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "previous backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(previousBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(previousBackdrop->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_RandomBackdrop) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "random backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + ctx->setRng(&rng); + + EXPECT_CALL(rng, randintExcept(0, 2, 1)).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + stage->setCostumeIndex(1); + code->run(ctx.get()); + ASSERT_EQ(stage->costumeIndex(), 0); + + EXPECT_CALL(rng, randintExcept(0, 2, 0)).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, false)); + code->reset(ctx.get()); + code->run(ctx.get()); + ASSERT_EQ(stage->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_RandomBackdropExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto randomBackdrop = std::make_shared("random backdrop", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(randomBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "random backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(randomBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(randomBackdrop->broadcast(), &thread, false)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_NumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", 3); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_PositiveOutOfRangeNumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", 5); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(numberBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_NegativeOutOfRangeNumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", -1); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(numberBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_InvalidNumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", std::numeric_limits::quiet_NaN()); + auto block = builder.currentBlock(); + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", std::numeric_limits::infinity()); + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", -std::numeric_limits::infinity()); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + stage->setCostumeIndex(2); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_StringIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", "3"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_StringIndexExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", "3"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(numberBackdrop->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_OutOfRangeStringIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", "+7.0"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_WhitespaceString) +{ + auto sprite = std::make_shared(); + sprite->setEngine(m_engine); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + m_engine->setTargets({ stage }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", ""); + builder.addBlock("looks_switchbackdropto"); + builder.addValueInput("BACKDROP", " "); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchBackdropTo_Stage) +{ + auto stage = std::make_shared(); + stage->setEngine(&m_engineMock); + + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_switchbackdropto"); + builder.addDropdownInput("BACKDROP", "backdrop1"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, false)); + stage->setCostumeIndex(2); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, GoToFrontBack_MoveSpriteToFront) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_gotofrontback"); + builder.addDropdownField("FRONT_BACK", "front"); + builder.build(); + + m_engine->moveDrawableToBack(sprite.get()); + + builder.run(); + std::initializer_list layers = { sprite->layerOrder(), sprite1->layerOrder(), sprite2->layerOrder(), sprite3->layerOrder() }; + ASSERT_EQ(sprite->layerOrder(), std::max(layers)); + + builder.run(); + ASSERT_EQ(sprite->layerOrder(), std::max(layers)); +} + +TEST_F(LooksBlocksTest, GoToFrontBack_MoveSpriteToBack) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_gotofrontback"); + builder.addDropdownField("FRONT_BACK", "back"); + builder.build(); + + m_engine->moveDrawableToFront(sprite.get()); + + builder.run(); + ASSERT_EQ(sprite->layerOrder(), 1); + + builder.run(); + ASSERT_EQ(sprite->layerOrder(), 1); +} + +TEST_F(LooksBlocksTest, GoToFrontBack_MoveStageToFront) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_gotofrontback"); + builder.addDropdownField("FRONT_BACK", "front"); + builder.build(); + + builder.run(); + ASSERT_EQ(stage->layerOrder(), 0); +} + +TEST_F(LooksBlocksTest, GoToFrontBack_MoveStageToBack) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_gotofrontback"); + builder.addDropdownField("FRONT_BACK", "back"); + builder.build(); + + builder.run(); + ASSERT_EQ(sprite->layerOrder(), 0); +} + +TEST_F(LooksBlocksTest, GoForwardBackwardLayers_MoveSpriteForwardPositive) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_goforwardbackwardlayers"); + builder.addDropdownField("FORWARD_BACKWARD", "forward"); + builder.addValueInput("NUM", 2); + builder.build(); + + // sprite, sprite1, sprite3, sprite2 + m_engine->moveDrawableToFront(sprite1.get()); + m_engine->moveDrawableToFront(sprite3.get()); + m_engine->moveDrawableToFront(sprite2.get()); + m_engine->moveDrawableToBack(sprite.get()); + + // sprite1, sprite3, sprite, sprite2 + builder.run(); + ASSERT_GT(sprite->layerOrder(), sprite1->layerOrder()); + ASSERT_GT(sprite->layerOrder(), sprite3->layerOrder()); + ASSERT_LT(sprite->layerOrder(), sprite2->layerOrder()); +} + +TEST_F(LooksBlocksTest, GoForwardBackwardLayers_MoveSpriteForwardNegative) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_goforwardbackwardlayers"); + builder.addDropdownField("FORWARD_BACKWARD", "forward"); + builder.addValueInput("NUM", -1); + builder.build(); + + // sprite1, sprite3, sprite2, sprite + m_engine->moveDrawableToFront(sprite1.get()); + m_engine->moveDrawableToFront(sprite3.get()); + m_engine->moveDrawableToFront(sprite2.get()); + m_engine->moveDrawableToFront(sprite.get()); + + // sprite1, sprite3, sprite, sprite2 + builder.run(); + ASSERT_GT(sprite->layerOrder(), sprite1->layerOrder()); + ASSERT_GT(sprite->layerOrder(), sprite3->layerOrder()); + ASSERT_LT(sprite->layerOrder(), sprite2->layerOrder()); +} + +TEST_F(LooksBlocksTest, GoForwardBackwardLayers_MoveSpriteBackwardPositive) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_goforwardbackwardlayers"); + builder.addDropdownField("FORWARD_BACKWARD", "backward"); + builder.addValueInput("NUM", 1); + builder.build(); + + // sprite1, sprite3, sprite2, sprite + m_engine->moveDrawableToFront(sprite1.get()); + m_engine->moveDrawableToFront(sprite3.get()); + m_engine->moveDrawableToFront(sprite2.get()); + m_engine->moveDrawableToFront(sprite.get()); + + // sprite1, sprite3, sprite, sprite2 + builder.run(); + ASSERT_GT(sprite->layerOrder(), sprite1->layerOrder()); + ASSERT_GT(sprite->layerOrder(), sprite3->layerOrder()); + ASSERT_LT(sprite->layerOrder(), sprite2->layerOrder()); +} + +TEST_F(LooksBlocksTest, GoForwardBackwardLayers_MoveSpriteBackwardNegative) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_goforwardbackwardlayers"); + builder.addDropdownField("FORWARD_BACKWARD", "backward"); + builder.addValueInput("NUM", -2); + builder.build(); + + // sprite, sprite1, sprite3, sprite2 + m_engine->moveDrawableToFront(sprite1.get()); + m_engine->moveDrawableToFront(sprite3.get()); + m_engine->moveDrawableToFront(sprite2.get()); + m_engine->moveDrawableToBack(sprite.get()); + + // sprite1, sprite3, sprite, sprite2 + builder.run(); + ASSERT_GT(sprite->layerOrder(), sprite1->layerOrder()); + ASSERT_GT(sprite->layerOrder(), sprite3->layerOrder()); + ASSERT_LT(sprite->layerOrder(), sprite2->layerOrder()); +} + +TEST_F(LooksBlocksTest, GoForwardBackwardLayers_MoveStageForward) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_goforwardbackwardlayers"); + builder.addDropdownField("FORWARD_BACKWARD", "forward"); + builder.addValueInput("NUM", 3); + builder.build(); + + builder.run(); + ASSERT_EQ(stage->layerOrder(), 0); +} + +TEST_F(LooksBlocksTest, GoForwardBackwardLayers_MoveStageBackward) +{ + auto stage = std::make_shared(); + auto sprite = std::make_shared(); + auto sprite1 = std::make_shared(); + auto sprite2 = std::make_shared(); + auto sprite3 = std::make_shared(); + m_engine->setTargets({ stage, sprite, sprite1, sprite2, sprite3 }); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_goforwardbackwardlayers"); + builder.addDropdownField("FORWARD_BACKWARD", "backward"); + builder.addValueInput("NUM", 3); + builder.build(); + + builder.run(); + ASSERT_EQ(stage->layerOrder(), 0); +} + +TEST_F(LooksBlocksTest, BackdropNumberName_SpriteBackdropNumber) +{ + auto sprite = std::make_shared(); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + m_engine->setTargets({ stage, sprite }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_backdropnumbername"); + builder.addDropdownField("NUMBER_NAME", "number"); + builder.captureBlockReturnValue(); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + + stage->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 3); + ASSERT_EQ(Value(list->data()[1]).toDouble(), 1); +} + +TEST_F(LooksBlocksTest, BackdropNumberName_SpriteBackdropName) +{ + auto sprite = std::make_shared(); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + m_engine->setTargets({ stage, sprite }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_backdropnumbername"); + builder.addDropdownField("NUMBER_NAME", "name"); + builder.captureBlockReturnValue(); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + + stage->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toString(), "test"); + ASSERT_EQ(Value(list->data()[1]).toString(), "backdrop1"); +} + +TEST_F(LooksBlocksTest, BackdropNumberName_StageBackdropNumber) +{ + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_backdropnumbername"); + builder.addDropdownField("NUMBER_NAME", "number"); + builder.captureBlockReturnValue(); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + + stage->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 3); + ASSERT_EQ(Value(list->data()[1]).toDouble(), 1); +} + +TEST_F(LooksBlocksTest, BackdropNumberName_StageBackdropName) +{ + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_backdropnumbername"); + builder.addDropdownField("NUMBER_NAME", "name"); + builder.captureBlockReturnValue(); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + + stage->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toString(), "test"); + ASSERT_EQ(Value(list->data()[1]).toString(), "backdrop1"); +} + +TEST_F(LooksBlocksTest, CostumeNumberName_SpriteCostumeNumber) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_costumenumbername"); + builder.addDropdownField("NUMBER_NAME", "number"); + builder.captureBlockReturnValue(); + builder.build(); + + sprite->setCostumeIndex(2); + builder.run(); + + sprite->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 3); + ASSERT_EQ(Value(list->data()[1]).toDouble(), 1); +} + +TEST_F(LooksBlocksTest, CostumeNumberName_SpriteCostumeName) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto costume2 = std::make_shared("costume2", "b", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(costume2); + sprite->addCostume(testCostume); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_costumenumbername"); + builder.addDropdownField("NUMBER_NAME", "name"); + builder.captureBlockReturnValue(); + builder.build(); + + sprite->setCostumeIndex(2); + builder.run(); + + sprite->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toString(), "test"); + ASSERT_EQ(Value(list->data()[1]).toString(), "costume1"); +} + +TEST_F(LooksBlocksTest, CostumeNumberName_StageCostumeNumber) +{ + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_costumenumbername"); + builder.addDropdownField("NUMBER_NAME", "number"); + builder.captureBlockReturnValue(); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + + stage->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 3); + ASSERT_EQ(Value(list->data()[1]).toDouble(), 1); +} + +TEST_F(LooksBlocksTest, CostumeNumberName_StageCostumeName) +{ + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_costumenumbername"); + builder.addDropdownField("NUMBER_NAME", "name"); + builder.captureBlockReturnValue(); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + + stage->setCostumeIndex(0); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 2); + ASSERT_EQ(Value(list->data()[0]).toString(), "test"); + ASSERT_EQ(Value(list->data()[1]).toString(), "backdrop1"); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_NoBackdrops) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "backdrop2"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts).Times(0); + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_BackdropName) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "backdrop2"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_InvalidBackdropName) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "invalid"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, true)); + stage->setCostumeIndex(1); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, true)); + stage->setCostumeIndex(2); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_NextBackdrop) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "next backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, true)); + stage->setCostumeIndex(0); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_NextBackdropExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto nextBackdrop = std::make_shared("next backdrop", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(nextBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "next backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(nextBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(nextBackdrop->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_PreviousBackdrop) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "previous backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, true)); + stage->setCostumeIndex(0); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop2->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_PreviousBackdropExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto previousBackdrop = std::make_shared("previous backdrop", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(previousBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "previous backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(previousBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(previousBackdrop->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_RandomBackdrop) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "random backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + ctx->setRng(&rng); + + EXPECT_CALL(rng, randintExcept(0, 2, 1)).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + stage->setCostumeIndex(1); + code->run(ctx.get()); + ASSERT_EQ(stage->costumeIndex(), 0); + + EXPECT_CALL(rng, randintExcept(0, 2, 0)).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, true)); + code->reset(ctx.get()); + code->run(ctx.get()); + ASSERT_EQ(stage->costumeIndex(), 2); + + ASSERT_TRUE(ctx->promise()); + ASSERT_FALSE(ctx->promise()->isResolved()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ctx->promise()->resolve(); + + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_RandomBackdropExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto randomBackdrop = std::make_shared("random backdrop", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(randomBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "random backdrop"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(randomBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + EXPECT_CALL(m_engineMock, startBackdropScripts(randomBackdrop->broadcast(), &thread, true)); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_NumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", 3); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(testBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_PositiveOutOfRangeNumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", 5); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(numberBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_NegativeOutOfRangeNumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", -1); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(numberBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_InvalidNumberIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", std::numeric_limits::quiet_NaN()); + auto block = builder.currentBlock(); + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", std::numeric_limits::infinity()); + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", -std::numeric_limits::infinity()); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + stage->setCostumeIndex(2); + thread.reset(); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_StringIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", "3"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_StringIndexExists) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", "3"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(numberBackdrop->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 1); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_OutOfRangeStringIndex) +{ + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", "+7.0"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_WhitespaceString) +{ + auto sprite = std::make_shared(); + sprite->setEngine(m_engine); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto numberBackdrop = std::make_shared("3", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(numberBackdrop); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + m_engine->setTargets({ stage }); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", ""); + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addValueInput("BACKDROP", " "); + builder.build(); + + stage->setCostumeIndex(2); + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 2); +} + +TEST_F(LooksBlocksTest, SwitchBackdropToAndWait_Stage) +{ + auto stage = std::make_shared(); + stage->setEngine(&m_engineMock); + + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + EXPECT_CALL(m_engineMock, stage()).WillRepeatedly(Return(stage.get())); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_switchbackdroptoandwait"); + builder.addDropdownInput("BACKDROP", "backdrop1"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, startBackdropScripts(backdrop1->broadcast(), &thread, true)); + stage->setCostumeIndex(2); + thread.run(); + ASSERT_EQ(stage->costumeIndex(), 0); + + ASSERT_TRUE(thread.promise()); + ASSERT_FALSE(thread.promise()->isResolved()); + ASSERT_FALSE(thread.isFinished()); + thread.promise()->resolve(); + + thread.run(); + ASSERT_TRUE(thread.isFinished()); +} + +TEST_F(LooksBlocksTest, NextBackdrop_Sprite) +{ + auto sprite = std::make_shared(); + auto costume1 = std::make_shared("costume1", "a", "png"); + auto testCostume = std::make_shared("test", "c", "svg"); + sprite->addCostume(costume1); + sprite->addCostume(testCostume); + + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + m_engine->setTargets({ stage, sprite }); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("looks_nextbackdrop"); + builder.build(); + + sprite->setCostumeIndex(0); + stage->setCostumeIndex(0); + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); + ASSERT_EQ(stage->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); + ASSERT_EQ(stage->costumeIndex(), 2); + + builder.run(); + ASSERT_EQ(sprite->costumeIndex(), 0); + ASSERT_EQ(stage->costumeIndex(), 0); +} + +TEST_F(LooksBlocksTest, NextBackdrop_Stage) +{ + auto stage = std::make_shared(); + auto backdrop1 = std::make_shared("backdrop1", "a", "png"); + auto backdrop2 = std::make_shared("backdrop2", "b", "png"); + auto testBackdrop = std::make_shared("test", "c", "svg"); + stage->addCostume(backdrop1); + stage->addCostume(backdrop2); + stage->addCostume(testBackdrop); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("looks_nextbackdrop"); + builder.build(); + + stage->setCostumeIndex(0); + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 1); + + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 2); + + builder.run(); + ASSERT_EQ(stage->costumeIndex(), 0); +} diff --git a/test/engine/engine_test.cpp b/test/engine/engine_test.cpp index 54480ccce..c1dab56d7 100644 --- a/test/engine/engine_test.cpp +++ b/test/engine/engine_test.cpp @@ -1321,6 +1321,11 @@ TEST(EngineTest, MoveDrawableForwardLayers) std::vector sprites; createTargets(&engine, sprites); + const auto &targets = engine.targets(); + + for (auto target : targets) + target->bubble()->setText("test"); + engine.moveDrawableForwardLayers(sprites[4], 2); ASSERT_EQ(sprites[0]->layerOrder(), 1); ASSERT_EQ(sprites[1]->layerOrder(), 5); @@ -1380,6 +1385,20 @@ TEST(EngineTest, MoveDrawableForwardLayers) ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 8); ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 9); ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 11); + + sprites[1]->bubble()->setText(""); + sprites[3]->bubble()->setText(""); + engine.moveDrawableForwardLayers(sprites[2], 8); + ASSERT_EQ(sprites[0]->layerOrder(), 1); + ASSERT_EQ(sprites[1]->layerOrder(), 4); + ASSERT_EQ(sprites[2]->layerOrder(), 11); + ASSERT_EQ(sprites[3]->layerOrder(), 3); + ASSERT_EQ(sprites[4]->layerOrder(), 2); + ASSERT_EQ(sprites[0]->bubble()->layerOrder(), 9); + ASSERT_EQ(sprites[1]->bubble()->layerOrder(), 6); + ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 7); + ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 8); + ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 10); } TEST(EngineTest, MoveDrawableBackwardLayers) @@ -1388,6 +1407,11 @@ TEST(EngineTest, MoveDrawableBackwardLayers) std::vector sprites; createTargets(&engine, sprites); + const auto &targets = engine.targets(); + + for (auto target : targets) + target->bubble()->setText("test"); + engine.moveDrawableBackwardLayers(sprites[4], -2); ASSERT_EQ(sprites[0]->layerOrder(), 1); ASSERT_EQ(sprites[1]->layerOrder(), 5); @@ -1447,6 +1471,20 @@ TEST(EngineTest, MoveDrawableBackwardLayers) ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 8); ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 9); ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 11); + + sprites[1]->bubble()->setText(""); + sprites[3]->bubble()->setText(""); + engine.moveDrawableBackwardLayers(sprites[2], -8); + ASSERT_EQ(sprites[0]->layerOrder(), 1); + ASSERT_EQ(sprites[1]->layerOrder(), 4); + ASSERT_EQ(sprites[2]->layerOrder(), 11); + ASSERT_EQ(sprites[3]->layerOrder(), 3); + ASSERT_EQ(sprites[4]->layerOrder(), 2); + ASSERT_EQ(sprites[0]->bubble()->layerOrder(), 9); + ASSERT_EQ(sprites[1]->bubble()->layerOrder(), 6); + ASSERT_EQ(sprites[2]->bubble()->layerOrder(), 7); + ASSERT_EQ(sprites[3]->bubble()->layerOrder(), 8); + ASSERT_EQ(sprites[4]->bubble()->layerOrder(), 10); } TEST(EngineTest, MoveDrawableBehindOther) diff --git a/test/llvm/llvmcodebuilder_test.cpp b/test/llvm/llvmcodebuilder_test.cpp index 47030a26b..1e88ce7a4 100644 --- a/test/llvm/llvmcodebuilder_test.cpp +++ b/test/llvm/llvmcodebuilder_test.cpp @@ -488,12 +488,18 @@ TEST_F(LLVMCodeBuilderTest, FunctionCalls) v = m_builder->addConstValue(321.5); m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + v = m_builder->addFunctionCall("test_const_number", Compiler::StaticType::Number, { Compiler::StaticType::Number }, { v }); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); v = m_builder->addConstValue("test"); m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + v = m_builder->addFunctionCall("test_const_string", Compiler::StaticType::String, { Compiler::StaticType::String }, { v }); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); v = m_builder->addConstValue(true); m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + v = m_builder->addFunctionCall("test_const_bool", Compiler::StaticType::Bool, { Compiler::StaticType::Bool }, { v }); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); auto code = m_builder->finalize(); Script script(&m_target, nullptr, nullptr); @@ -519,7 +525,10 @@ TEST_F(LLVMCodeBuilderTest, FunctionCalls) "123\n" "1\n" "321.5\n" + "321.5\n" "test\n" + "test\n" + "true\n" "true\n"; EXPECT_CALL(m_target, isStage()).Times(7); @@ -2097,6 +2106,56 @@ TEST_F(LLVMCodeBuilderTest, ReadVariable) ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); } +TEST_F(LLVMCodeBuilderTest, CastNonRawValueToUnknownType) +{ + Stage stage; + Sprite sprite; + sprite.setEngine(&m_engine); + EXPECT_CALL(m_engine, stage()).WillRepeatedly(Return(&stage)); + + auto var = std::make_shared("", "", 87); + stage.addVariable(var); + + createBuilder(&sprite, true); + + // Unknown type + CompilerValue *v = m_builder->addVariableValue(var.get()); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + + // Number + v = m_builder->addConstValue(23.5); + m_builder->createVariableWrite(var.get(), v); + v = m_builder->addVariableValue(var.get()); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + + // String + v = m_builder->addConstValue("Hello world"); + m_builder->createVariableWrite(var.get(), v); + v = m_builder->addVariableValue(var.get()); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + + // Bool + v = m_builder->addConstValue(true); + m_builder->createVariableWrite(var.get(), v); + v = m_builder->addVariableValue(var.get()); + m_builder->addFunctionCall("test_print_unknown", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { v }); + + std::string expected; + expected += var->value().toString() + '\n'; + expected += "23.5\n"; + expected += "Hello world\n"; + expected += "true\n"; + + auto code = m_builder->finalize(); + Script script(&sprite, nullptr, nullptr); + script.setCode(code); + Thread thread(&sprite, nullptr, &script); + auto ctx = code->createExecutionContext(&thread); + testing::internal::CaptureStdout(); + code->run(ctx.get()); + ASSERT_EQ(testing::internal::GetCapturedStdout(), expected); +} + TEST_F(LLVMCodeBuilderTest, ClearList) { Stage stage; diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp index 5749896a4..23bb9020b 100644 --- a/test/scratch_classes/sprite_test.cpp +++ b/test/scratch_classes/sprite_test.cpp @@ -904,4 +904,9 @@ TEST(SpriteTest, BubbleTextRedraw) EXPECT_CALL(engine, requestRedraw).Times(0); sprite.bubble()->setText(""); + + EXPECT_CALL(engine, requestRedraw()); + sprite.setVisible(true); + EXPECT_CALL(engine, requestRedraw).Times(0); + sprite.bubble()->setText(""); } diff --git a/test/scratch_classes/textbubble_test.cpp b/test/scratch_classes/textbubble_test.cpp index 77f30bc15..e07455a6d 100644 --- a/test/scratch_classes/textbubble_test.cpp +++ b/test/scratch_classes/textbubble_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "../common.h" @@ -33,15 +34,23 @@ TEST(TextBubbleTest, BubbleText) { TextBubble bubble; ASSERT_TRUE(bubble.text().empty()); + ASSERT_EQ(bubble.owner(), nullptr); bubble.setText("hello"); ASSERT_EQ(bubble.text(), "hello"); + ASSERT_EQ(bubble.owner(), nullptr); + + Thread thread(nullptr, nullptr, nullptr); + bubble.setText("test", &thread); + ASSERT_EQ(bubble.text(), "test"); + ASSERT_EQ(bubble.owner(), &thread); EngineMock engine; bubble.setEngine(&engine); EXPECT_CALL(engine, moveDrawableToFront(&bubble)); bubble.setText("world"); ASSERT_EQ(bubble.text(), "world"); + ASSERT_EQ(bubble.owner(), nullptr); bubble.setEngine(nullptr); // longstr.length = 384, should be limited to 330 in bubble text