diff --git a/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/AiTutorSpriteValidatorTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/AiTutorSpriteValidatorTest.kt new file mode 100644 index 00000000000..1aa10c89385 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/AiTutorSpriteValidatorTest.kt @@ -0,0 +1,119 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.robolectric.aiassist + +import android.os.Build +import org.catrobat.catroid.ProjectManager +import org.catrobat.catroid.content.Project +import org.catrobat.catroid.content.Script +import org.catrobat.catroid.content.Sprite +import org.catrobat.catroid.content.StartScript +import org.catrobat.catroid.content.bricks.Brick +import org.catrobat.catroid.content.bricks.SetXBrick +import org.catrobat.catroid.content.bricks.SetYBrick +import org.catrobat.catroid.ui.SpriteActivity +import org.catrobat.catroid.ui.aiassist.validation.AiTutorSpriteValidator +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) +class AiTutorSpriteValidatorTest { + + private lateinit var activity: SpriteActivity + + @Before + fun setUp() { + val controller = Robolectric.buildActivity(SpriteActivity::class.java) + activity = controller.get() + val project = Project(activity, javaClass.simpleName) + val hostSprite = Sprite("hostSprite") + project.defaultScene.addSprite(hostSprite) + ProjectManager.getInstance().apply { + currentProject = project + currentSprite = hostSprite + currentlyEditedScene = project.defaultScene + } + controller.create().resume() + } + + @After + fun tearDown() { + ProjectManager.getInstance().resetProjectManager() + } + + @Test + fun completeSpriteIsValid() { + val result = AiTutorSpriteValidator.validate(spriteWith(SetXBrick()), activity) + + assertTrue(result is AiTutorSpriteValidator.Result.Valid) + } + + @Test + fun spriteWithNoBricksIsValid() { + val result = AiTutorSpriteValidator.validate(spriteWith(), activity) + + assertTrue(result is AiTutorSpriteValidator.Result.Valid) + } + + @Test + fun spriteWithMultipleCompleteScriptsIsValid() { + val sprite = Sprite("testSprite") + sprite.addScript(StartScript().apply { addBrick(SetXBrick()) }) + sprite.addScript(StartScript().apply { addBrick(SetYBrick()) }) + + val result = AiTutorSpriteValidator.validate(sprite, activity) + + assertTrue(result is AiTutorSpriteValidator.Result.Valid) + } + + @Test + fun brickMissingRequiredFormulaIsInvalidWithSpecificReason() { + val brick = SetXBrick() + // Simulate AI output that parses but omits a required value: the field is still declared in + // brickFieldToTextViewIdMap but has no entry in formulaMap, which crashes at render time. + brick.formulaMap.remove(Brick.BrickField.X_POSITION) + + val result = AiTutorSpriteValidator.validate(spriteWith(brick), activity) + + assertTrue(result is AiTutorSpriteValidator.Result.Invalid) + val reason = (result as AiTutorSpriteValidator.Result.Invalid).reason + assertTrue(reason.contains("SetXBrick")) + assertTrue(reason.contains("X_POSITION")) + } + + private fun spriteWith(vararg bricks: Brick): Sprite { + val sprite = Sprite("testSprite") + val script: Script = StartScript() + bricks.forEach { script.addBrick(it) } + sprite.addScript(script) + return sprite + } +} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/BrickTextFormatterTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/BrickTextFormatterTest.kt new file mode 100644 index 00000000000..f97c42ea45a --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/BrickTextFormatterTest.kt @@ -0,0 +1,158 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.robolectric.aiassist + +import android.os.Build +import org.catrobat.catroid.ProjectManager +import org.catrobat.catroid.common.LookData +import org.catrobat.catroid.content.Project +import org.catrobat.catroid.content.Sprite +import org.catrobat.catroid.content.bricks.IfLogicBeginBrick +import org.catrobat.catroid.content.bricks.PlaceAtBrick +import org.catrobat.catroid.content.bricks.SetLookBrick +import org.catrobat.catroid.content.bricks.SetVariableBrick +import org.catrobat.catroid.content.bricks.SetXBrick +import org.catrobat.catroid.content.bricks.WaitBrick +import org.catrobat.catroid.content.bricks.WhenStartedBrick +import org.catrobat.catroid.formulaeditor.Formula +import org.catrobat.catroid.formulaeditor.UserVariable +import org.catrobat.catroid.ui.SpriteActivity +import org.catrobat.catroid.ui.aiassist.diff.brickEditorLabel +import org.catrobat.catroid.ui.aiassist.diff.brickPhraseTokens +import org.catrobat.catroid.ui.aiassist.diff.dataNames +import org.catrobat.catroid.ui.aiassist.diff.scriptHeaderTitle +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) +class BrickTextFormatterTest { + + private lateinit var activity: SpriteActivity + + @Before + fun setUp() { + val controller = Robolectric.buildActivity(SpriteActivity::class.java) + activity = controller.get() + val project = Project(activity, javaClass.simpleName) + val hostSprite = Sprite("hostSprite") + project.defaultScene.addSprite(hostSprite) + ProjectManager.getInstance().apply { + currentProject = project + currentSprite = hostSprite + currentlyEditedScene = project.defaultScene + } + controller.create().resume() + } + + @After + fun tearDown() { + ProjectManager.getInstance().resetProjectManager() + } + + // ── dataNames ── + + @Test + fun dataNamesReturnsSelectedVariableName() { + val brick = SetVariableBrick(Formula(5.0), UserVariable("score")) + assertEquals(listOf("score"), dataNames(brick)) + } + + @Test + fun dataNamesIsEmptyForBrickWithoutData() { + assertTrue(dataNames(SetXBrick(5)).isEmpty()) + } + + @Test + fun dataNamesIncludesSelectedSpinnerName() { + val brick = SetLookBrick().apply { look = LookData().apply { name = "look1" } } + assertEquals(listOf("look1"), dataNames(brick)) + } + + // ── brickEditorLabel ── + + @Test + fun editorLabelRendersMultiPartBrickAsEditorPhrase() { + assertEquals("If 0 is true then", brickEditorLabel(IfLogicBeginBrick(), activity)) + } + + @Test + fun editorLabelShowsFieldValueInline() { + assertEquals("Set x to 0", brickEditorLabel(SetXBrick(0), activity)) + } + + @Test + fun editorLabelLabelsEachFieldByItsLayoutPosition() { + assertEquals("Place at x: 0 y: 500", brickEditorLabel(PlaceAtBrick(0, 500), activity)) + } + + @Test + fun editorLabelIncludesRuntimeUnitLabel() { + assertEquals("Wait 5 seconds", brickEditorLabel(WaitBrick(Formula(5.0)), activity)) + } + + @Test + fun editorLabelInlinesSpinnerSelectionAndValue() { + val brick = SetVariableBrick(Formula(0.0), UserVariable("playerY")) + assertEquals("Set variable playerY to 0", brickEditorLabel(brick, activity)) + } + + @Test + fun editorLabelInlinesSpinnerOnlySelection() { + val brick = SetLookBrick().apply { look = LookData().apply { name = "look1" } } + assertEquals("Switch to look look1", brickEditorLabel(brick, activity)) + } + + // ── brickPhraseTokens ── + + @Test + fun phraseTokensFlagTheChangedValueAsDynamic() { + val tokens = brickPhraseTokens(SetXBrick(100), activity, SetXBrick(0)) + + val value = tokens.single { it.dynamic } + assertEquals("100", value.text) + assertTrue(value.changed) + assertTrue(tokens.any { !it.dynamic && it.text == "Set x to" && !it.changed }) + } + + @Test + fun phraseTokensDoNotFlagAnUnchangedValue() { + val tokens = brickPhraseTokens(SetXBrick(100), activity, SetXBrick(100)) + assertTrue(tokens.none { it.changed }) + } + + // ── scriptHeaderTitle ── + + @Test + fun scriptHeaderTitleUsesTheRealEditorLabel() { + assertEquals("When scene starts", scriptHeaderTitle(WhenStartedBrick(), activity)) + } +} diff --git a/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/SpriteDifferTest.kt b/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/SpriteDifferTest.kt new file mode 100644 index 00000000000..66d6c3932d6 --- /dev/null +++ b/catroid/src/test/java/org/catrobat/catroid/test/robolectric/aiassist/SpriteDifferTest.kt @@ -0,0 +1,136 @@ +/* + * Catroid: An on-device visual programming system for Android devices + * Copyright (C) 2010-2026 The Catrobat Team + * () + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * An additional term exception under section 7 of the GNU Affero + * General Public License, version 3, is available at + * http://developer.catrobat.org/license_additional_term + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.catrobat.catroid.test.robolectric.aiassist + +import android.os.Build +import org.catrobat.catroid.ProjectManager +import org.catrobat.catroid.content.Project +import org.catrobat.catroid.content.Script +import org.catrobat.catroid.content.Sprite +import org.catrobat.catroid.content.StartScript +import org.catrobat.catroid.content.bricks.Brick +import org.catrobat.catroid.content.bricks.SetXBrick +import org.catrobat.catroid.content.bricks.SetYBrick +import org.catrobat.catroid.ui.SpriteActivity +import org.catrobat.catroid.ui.aiassist.diff.DiffStatus +import org.catrobat.catroid.ui.aiassist.diff.buildDiffRows +import org.catrobat.catroid.ui.aiassist.diff.isScriptHeaderRow +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) +class SpriteDifferTest { + + private lateinit var activity: SpriteActivity + + @Before + fun setUp() { + val controller = Robolectric.buildActivity(SpriteActivity::class.java) + activity = controller.get() + val project = Project(activity, javaClass.simpleName) + val hostSprite = Sprite("hostSprite") + project.defaultScene.addSprite(hostSprite) + ProjectManager.getInstance().apply { + currentProject = project + currentSprite = hostSprite + currentlyEditedScene = project.defaultScene + } + controller.create().resume() + } + + @After + fun tearDown() { + ProjectManager.getInstance().resetProjectManager() + } + + @Test + fun identicalSpritesAreAllUnchanged() { + val statuses = diffStatuses( + spriteWith(SetXBrick(), SetYBrick()), + spriteWith(SetXBrick(), SetYBrick()) + ) + + assertEquals(listOf(DiffStatus.UNCHANGED, DiffStatus.UNCHANGED), statuses) + } + + @Test + fun appendedBrickIsAdded() { + val statuses = diffStatuses( + spriteWith(SetXBrick()), + spriteWith(SetXBrick(), SetYBrick()) + ) + + assertEquals(listOf(DiffStatus.UNCHANGED, DiffStatus.ADDED), statuses) + } + + @Test + fun deletedBrickIsRemoved() { + val statuses = diffStatuses( + spriteWith(SetXBrick(), SetYBrick()), + spriteWith(SetXBrick()) + ) + + assertEquals(listOf(DiffStatus.UNCHANGED, DiffStatus.REMOVED), statuses) + } + + @Test + fun sameBrickWithChangedValueIsModified() { + val statuses = diffStatuses( + spriteWith(SetXBrick(0)), + spriteWith(SetXBrick(100)) + ) + + assertEquals(listOf(DiffStatus.MODIFIED), statuses) + } + + @Test + fun differentBrickClassesAreNotMergedIntoModified() { + val statuses = diffStatuses( + spriteWith(SetXBrick()), + spriteWith(SetYBrick()) + ) + + assertEquals(listOf(DiffStatus.REMOVED, DiffStatus.ADDED), statuses) + } + + private fun diffStatuses(old: Sprite, new: Sprite): List = + buildDiffRows(old, new, activity) + .filterNot { isScriptHeaderRow(it) } + .map { it.status } + + private fun spriteWith(vararg bricks: Brick): Sprite { + val sprite = Sprite("testSprite") + val script: Script = StartScript() + bricks.forEach { script.addBrick(it) } + sprite.addScript(script) + return sprite + } +}