diff --git a/.junie/guidelines.md b/.junie/guidelines.md index a9e0fab..626a8e2 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -47,6 +47,7 @@ This is an IntelliJ Platform plugin project for Moodle development support. ``` ## Testing +- Follow [Testing Docs](https://plugins.jetbrains.com/docs/intellij/testing-plugins.html) and his links to learn more about testing and writing tests. - Run tests: ```bash ./gradlew test diff --git a/CHANGELOG.md b/CHANGELOG.md index aac047f..e43df27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0). ## [Unreleased] +### Added +- Unit and UI tests to verify Composer "Synchronize IDE settings with composer.json" is disabled when enabling the Moodle framework. + +## [2.2.1] - 2025-10-16 + +### Fixed +- When enabling the Moodle framework, the plugin now programmatically disables PHP > Composer > "Synchronize IDE settings with composer.json" to prevent unintended overwrites of IDE configuration. + ### Added - Bundled Moodle inspection profile and registered it in `plugin.xml` so it becomes available after plugin installation. @@ -222,7 +230,8 @@ Add support for PHPStorm 2022.2 - Add live Template for Moodle $ADMIN by type ADMIN - Add Moodle code style for predefined code styles for PHP/Javascript/SCSS/LESS -[Unreleased]: https://github.com/SysBind/moodle-dev/compare/2.2.0...HEAD +[Unreleased]: https://github.com/SysBind/moodle-dev/compare/2.2.1...HEAD +[2.2.1]: https://github.com/SysBind/moodle-dev/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/SysBind/moodle-dev/compare/2.1.1...2.2.0 [2.1.1]: https://github.com/SysBind/moodle-dev/compare/2.1.0...2.1.1 [2.1.0]: https://github.com/SysBind/moodle-dev/compare/v2.0.0...2.1.0 diff --git a/README.md b/README.md index 8f2596f..ce1745c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Moodle Development Plugin for IntelliJ IDEA +ull# Moodle Development Plugin for IntelliJ IDEA ![Build](https://github.com/SysBind/moodle-dev/workflows/Build/badge.svg) ![Version](https://img.shields.io/jetbrains/plugin/v/16702) diff --git a/src/main/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsForm.kt b/src/main/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsForm.kt index ad3deed..45eb127 100644 --- a/src/main/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsForm.kt +++ b/src/main/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsForm.kt @@ -82,6 +82,15 @@ class MoodleSettingsForm(val project: Project) : PhpFrameworkConfigurable { settings.userName = userName.component.text settings.userEmail = userEmail.component.text + // If Moodle framework is enabled, ensure Composer IDE sync is disabled + if (settings.pluginEnabled) { + try { + il.co.sysbind.intellij.moodledev.util.PhpComposerSettingsUtil.disableComposerSync(project) + } catch (t: Throwable) { + log.warn("Unable to disable Composer sync via utility: ${t.message}") + } + } + // Configure PHP_Codesniffer if plugin is enabled if (settings.pluginEnabled) { // Check if composer is available diff --git a/src/main/kotlin/il/co/sysbind/intellij/moodledev/util/PhpComposerSettingsUtil.kt b/src/main/kotlin/il/co/sysbind/intellij/moodledev/util/PhpComposerSettingsUtil.kt new file mode 100644 index 0000000..22dbc5f --- /dev/null +++ b/src/main/kotlin/il/co/sysbind/intellij/moodledev/util/PhpComposerSettingsUtil.kt @@ -0,0 +1,101 @@ +package il.co.sysbind.intellij.moodledev.util + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project + +/** + * Utilities for interacting with the PHP Composer settings without taking a hard compile-time dependency + * on internal APIs that may change between PHP plugin versions. + * + * Goal: ensure "Synchronize IDE settings with composer.json" is disabled when Moodle framework is enabled. + */ +object PhpComposerSettingsUtil { + private val log = Logger.getInstance(PhpComposerSettingsUtil::class.java) + + /** + * Try to disable Composer auto-synchronization of IDE settings with composer.json. + * Uses reflection to remain compatible across PHP plugin versions where the API name may differ. + */ + fun disableComposerSync(project: Project) { + try { + // Try primary known settings class + val clazz = try { + Class.forName("com.jetbrains.php.composer.ComposerSettings") + } catch (cnf: ClassNotFoundException) { + // Fallback: some versions might use different package/name; keep room for expansion + log.warn("ComposerSettings class not found: ${cnf.message}") + null + } ?: return + + // Obtain getInstance(Project) if available, else fall back to no-arg getInstance() + val instance = try { + val withProject = clazz.methods.firstOrNull { + it.name.equals("getInstance", true) && it.parameterCount == 1 && Project::class.java.isAssignableFrom(it.parameterTypes[0]) + } + val noArg = clazz.methods.firstOrNull { it.name.equals("getInstance", true) && it.parameterCount == 0 } + when { + withProject != null -> withProject.invoke(null, project) + noArg != null -> noArg.invoke(null) + else -> null + } + } catch (e: Exception) { + log.warn("Failed to obtain ComposerSettings instance: ${e.message}") + null + } ?: return + + // 1) Prefer enum-based API: setSynchronizationState(SynchronizationState.DONT_SYNCHRONIZE) + try { + val enumClass = try { + Class.forName("com.jetbrains.php.composer.SynchronizationState") + } catch (e: ClassNotFoundException) { + null + } + val setState = clazz.methods.firstOrNull { m -> + m.name.equals("setSynchronizationState", true) && m.parameterCount == 1 && (enumClass == null || m.parameterTypes[0].isEnum) + } + if (setState != null) { + val dont = enumClass?.enumConstants?.firstOrNull { (it as Enum<*>).name.equals("DONT_SYNCHRONIZE", true) } + ?: setState.parameterTypes[0].enumConstants.firstOrNull { (it as Enum<*>).name.contains("DONT", true) } + if (dont != null) { + setState.isAccessible = true + setState.invoke(instance, dont) + log.info("Disabled Composer sync via setSynchronizationState(DONT_SYNCHRONIZE).") + return + } + } + } catch (ignore: Throwable) { + // fall through to boolean-based API + } + + // 2) Boolean-based API fallbacks: look for setter containing "sync" + val candidates = clazz.methods.filter { method -> + method.name.startsWith("set") && + method.parameterCount == 1 && + (method.parameterTypes[0] == java.lang.Boolean.TYPE || method.parameterTypes[0] == java.lang.Boolean::class.java) && + method.name.contains("sync", ignoreCase = true) + } + + // If no obvious candidates, try some well-known names explicitly to be safe in future refactors + val preferredNames = listOf( + "setSynchronizeWithComposerJson", + "setSynchronizeIdeSettingsWithComposerJson", + "setSyncWithComposerJson", + "setAutoSyncEnabled", + "setSynchronizeSettings" + ) + + val method = candidates.firstOrNull { m -> preferredNames.any { pn -> m.name.equals(pn, ignoreCase = true) } } + ?: candidates.firstOrNull() + + if (method != null) { + method.isAccessible = true + method.invoke(instance, false) + log.info("Disabled PHP Composer synchronization with composer.json via ${method.name}().") + } else { + log.warn("Could not find a suitable Composer sync setting method to invoke.") + } + } catch (t: Throwable) { + log.warn("Failed to disable Composer sync: ${t.message}") + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 74132dc..92483e2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -2,7 +2,7 @@ il.co.sysbind.intellij.moodledev Moodle Development - 2.2.0 + 2.2.1 SysBind Plugin For Moodle Developers diff --git a/src/test/kotlin/com/jetbrains/php/composer/ComposerSettings.kt b/src/test/kotlin/com/jetbrains/php/composer/ComposerSettings.kt new file mode 100644 index 0000000..326f46e --- /dev/null +++ b/src/test/kotlin/com/jetbrains/php/composer/ComposerSettings.kt @@ -0,0 +1,56 @@ +package com.jetbrains.php.composer + +import com.intellij.openapi.project.Project + +/** + * Test double for the PHP plugin's ComposerSettings class. + * Provides a minimal surface for PhpComposerSettingsUtil to reflect on. + */ +class ComposerSettings private constructor(val project: Project? = null) { + var synchronizeWithComposerJson: Boolean = true + var synchronizationState: SynchronizationState = SynchronizationState.SYNCHRONIZE + + // Enum mirrors real plugin concept for workspace.xml persistence + enum class SynchronizationState { SYNCHRONIZE, DONT_SYNCHRONIZE } + + // Preferred newer API + fun setSynchronizationState(state: SynchronizationState) { + synchronizationState = state + // Keep boolean flag consistent with enum semantics + synchronizeWithComposerJson = state == SynchronizationState.SYNCHRONIZE + } + + // Alternative setter name to ensure our reflection fallback remains covered + fun setAutoSyncEnabled(value: Boolean) { + synchronizeWithComposerJson = value + synchronizationState = if (value) SynchronizationState.SYNCHRONIZE else SynchronizationState.DONT_SYNCHRONIZE + } + + companion object { + @JvmStatic + private var lastInstance: ComposerSettings? = null + + @JvmStatic + fun getInstance(project: Project): ComposerSettings { + val inst = ComposerSettings(project) + lastInstance = inst + return inst + } + + @JvmStatic + fun getInstance(): ComposerSettings { + val inst = ComposerSettings(null) + lastInstance = inst + return inst + } + + @JvmStatic + fun getLastInstance(): ComposerSettings? = lastInstance + + // Test-only utility to reset singleton-like state between tests + @JvmStatic + fun clearLastInstanceForTests() { + lastInstance = null + } + } +} diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleComposerSyncEnumTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleComposerSyncEnumTest.kt new file mode 100644 index 0000000..8596907 --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleComposerSyncEnumTest.kt @@ -0,0 +1,28 @@ +package il.co.sysbind.intellij.moodledev.project + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.composer.ComposerSettings +import il.co.sysbind.intellij.moodledev.util.PhpComposerSettingsUtil +import org.junit.Test + +class MoodleComposerSyncEnumTest : BasePlatformTestCase() { + + @Test + fun testDisableComposerSync_UsesEnumWhenAvailable() { + // Precondition: test double provides enum-based API + ComposerSettings.clearLastInstanceForTests() + + // Act + PhpComposerSettingsUtil.disableComposerSync(project) + + // Assert + val settings = ComposerSettings.getLastInstance() + assertNotNull("ComposerSettings should have been instantiated via reflection", settings) + assertEquals( + "SynchronizationState should be DONT_SYNCHRONIZE", + ComposerSettings.SynchronizationState.DONT_SYNCHRONIZE, + settings!!.synchronizationState + ) + assertFalse("Boolean mirror should be false when enum is DONT_SYNCHRONIZE", settings.synchronizeWithComposerJson) + } +} diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsComposerSyncTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsComposerSyncTest.kt new file mode 100644 index 0000000..a328026 --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsComposerSyncTest.kt @@ -0,0 +1,36 @@ +package il.co.sysbind.intellij.moodledev.project + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.composer.ComposerSettings +import il.co.sysbind.intellij.moodledev.MoodleBundle +import org.junit.Test + +class MoodleSettingsComposerSyncTest : BasePlatformTestCase() { + + @Test + fun testApply_DisablesComposerSyncWhenFrameworkEnabled() { + // Arrange + val projectSettings = project.getService(MoodleProjectSettings::class.java) + projectSettings.settings.pluginEnabled = false + val form = MoodleSettingsForm(project) + // create UI to initialize components + val component = form.createComponent() + assertNotNull("Settings form component should be created", component) + // Ensure initial state false + assertFalse("Precondition: plugin should be disabled", form.pluginEnabled.component.isSelected) + + // Act: user enables the framework and clicks Apply + form.pluginEnabled.component.isSelected = true + form.apply() + + // Assert: state persisted and composer sync disabled via test double + assertTrue("Plugin should be enabled after apply", projectSettings.settings.pluginEnabled) + val instance = ComposerSettings.getLastInstance() + assertNotNull("ComposerSettings test double should have been instantiated", instance) + assertFalse("Composer sync should be disabled when enabling framework", instance!!.synchronizeWithComposerJson) + + // UI bits sanity: display name/id available (lightweight UI test) + assertTrue(MoodleBundle.getMessage("configurable.name").isNotBlank()) + assertTrue(form.getId().isNotBlank()) + } +} diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsUiToggleTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsUiToggleTest.kt new file mode 100644 index 0000000..86974df --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/project/MoodleSettingsUiToggleTest.kt @@ -0,0 +1,42 @@ +package il.co.sysbind.intellij.moodledev.project + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.composer.ComposerSettings +import org.junit.Test + +/** + * Lightweight UI-ish test exercising the Configurable form apply() path. + * It simulates a user enabling the Moodle framework via the Settings page + * and verifies that Composer sync is disabled using our reflection bridge. + */ +class MoodleSettingsUiToggleTest : BasePlatformTestCase() { + + @Test + fun testEnablingFrameworkDisablesComposerSync_Idempotent() { + // Arrange + val form = MoodleSettingsForm(project) + form.createComponent() // initialize form components + + // Enable and apply twice to ensure idempotency + form.pluginEnabled.component.isSelected = true + form.apply() + form.apply() + + val cs = ComposerSettings.getLastInstance() + assertNotNull("ComposerSettings test instance should exist after apply()", cs) + assertFalse("Composer sync should be disabled after enabling framework", + cs!!.synchronizeWithComposerJson) + assertEquals("Enum state should be DONT_SYNCHRONIZE", + ComposerSettings.SynchronizationState.DONT_SYNCHRONIZE, + cs.synchronizationState) + + // Now disable framework and apply; we do not re-enable sync automatically. + form.pluginEnabled.component.isSelected = false + form.apply() + + // Composer sync should remain disabled (plugin does not auto-enable it on disable) + val cs2 = ComposerSettings.getLastInstance() + assertFalse("Composer sync should remain disabled after disabling framework", + cs2!!.synchronizeWithComposerJson) + } +} diff --git a/src/test/kotlin/il/co/sysbind/intellij/moodledev/util/PhpComposerSettingsUtilTest.kt b/src/test/kotlin/il/co/sysbind/intellij/moodledev/util/PhpComposerSettingsUtilTest.kt new file mode 100644 index 0000000..c6e0311 --- /dev/null +++ b/src/test/kotlin/il/co/sysbind/intellij/moodledev/util/PhpComposerSettingsUtilTest.kt @@ -0,0 +1,32 @@ +package il.co.sysbind.intellij.moodledev.util + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.composer.ComposerSettings +import org.junit.Test + +class PhpComposerSettingsUtilTest : BasePlatformTestCase() { + + override fun setUp() { + super.setUp() + // Reset shared state of the fake ComposerSettings between tests + ComposerSettings.clearLastInstanceForTests() + } + + @Test + fun testDisableComposerSync_DisablesOnFakeComposerSettings() { + // Ensure there's no lingering instance from other tests + val before = ComposerSettings.getLastInstance() + assertNull("Precondition: last instance should be null or irrelevant", before) + + // Act + PhpComposerSettingsUtil.disableComposerSync(project) + + // Assert + val instance = ComposerSettings.getLastInstance() + assertNotNull("ComposerSettings instance should be created by reflection", instance) + assertFalse( + "Composer synchronization must be disabled when called", + instance!!.synchronizeWithComposerJson + ) + } +}