Skip to content

Commit

Permalink
Add tests for configurable and editor
Browse files Browse the repository at this point in the history
  • Loading branch information
FWDekker committed Oct 22, 2023
1 parent e960a9d commit 0af6ce3
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 95 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,10 @@ See [Plugin Signing](https://plugins.jetbrains.com/docs/intellij/plugin-signing.

### 🧪 Quality assurance
```bash
$ gradlew test # Run tests
$ gradlew test # Run tests (and collect coverage)
$ gradlew test --tests X # Run tests in class X (package name optional)
$ gradlew test -Pkotest.tags="X" # Run tests with `NamedTag` X (also supports not (!), and (&), or (|))
$ gradlew koverHtmlReport # Create HTML coverage report for previous test run
$ gradlew check # Run tests and static analysis
$ gradlew runPluginVerifier # Check for compatibility issues
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,6 @@ class TemplateSettingsAction(private val template: Template? = null) : AnAction(
override fun actionPerformed(event: AnActionEvent) =
ShowSettingsUtil.getInstance()
.showSettingsDialog(event.project, TemplateListConfigurable::class.java) { configurable ->
configurable?.also { it.templateToSelect = template?.uuid }
configurable?.also { it.schemeToSelect = template?.uuid }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import javax.swing.JComponent
/**
* Tells IntelliJ how to use a [TemplateListEditor] to edit a [TemplateList] in the settings dialog.
*
* Set [templateToSelect] before [createComponent] is invoked to determine which template should be selected when the
* Set [schemeToSelect] before [createComponent] is invoked to determine which template should be selected when the
* configurable opens.
*
* This class is separate from [TemplateListEditor] because that class creates UI components in the constructor. But
Expand All @@ -28,9 +28,23 @@ class TemplateListConfigurable : Configurable, Disposable {
lateinit var editor: TemplateListEditor private set

/**
* The UUID of the template to select after calling [createComponent].
* The UUID of the scheme to select after calling [createComponent].
*/
var templateToSelect: String? = null
var schemeToSelect: String? = null


/**
* Returns the name of the configurable as displayed in the settings window.
*/
override fun getDisplayName() = "Randomness"

/**
* Creates a new editor and returns the root pane of the created editor.
*/
override fun createComponent(): JComponent {
editor = TemplateListEditor(initialSelection = schemeToSelect).also { Disposer.register(this, it) }
return editor.rootComponent
}


/**
Expand Down Expand Up @@ -61,6 +75,7 @@ class TemplateListConfigurable : Configurable, Disposable {
*/
override fun reset() = editor.reset()


/**
* Recursively disposes this configurable's resources.
*/
Expand All @@ -70,20 +85,4 @@ class TemplateListConfigurable : Configurable, Disposable {
* Non-recursively disposes this configurable's resources.
*/
override fun dispose() = Unit


/**
* Creates a new editor and returns the root pane of the created editor.
*/
override fun createComponent(): JComponent {
editor = TemplateListEditor().also { Disposer.register(this, it) }
editor.queueSelection = templateToSelect
return editor.rootComponent
}


/**
* Returns the name of the configurable as displayed in the settings window.
*/
override fun getDisplayName() = "Randomness"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import com.fwdekker.randomness.uuid.UuidSchemeEditor
import com.fwdekker.randomness.word.WordScheme
import com.fwdekker.randomness.word.WordSchemeEditor
import com.intellij.openapi.Disposable
import com.intellij.openapi.ui.Splitter
import com.intellij.openapi.util.Disposer
import com.intellij.ui.JBSplitter
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.JBEmptyBorder
Expand All @@ -42,9 +42,14 @@ import javax.swing.SwingUtilities
* contained within them, and on the right, a [SchemeEditor] for the currently-selected template or scheme is shown.
*
* @property originalTemplateList The templates to edit.
* @param initialSelection the UUID of the scheme to select initially, or `null` or an invalid UUID to select the first
* template
* @see TemplateListConfigurable
*/
class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAULT.templateList) : Disposable {
class TemplateListEditor(
val originalTemplateList: TemplateList = Settings.DEFAULT.templateList,
initialSelection: String? = null,
) : Disposable {
/**
* The root component of the editor.
*/
Expand All @@ -58,16 +63,9 @@ class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAU
PreviewPanel { templateTree.selectedTemplate ?: StringScheme("") }
.also { Disposer.register(this, it) }

/**
* The UUID of the scheme to select after the next invocation of [reset].
*
* @see TemplateListConfigurable
*/
var queueSelection: String? = null


init {
val splitter = createSplitter(false, SPLITTER_PROPORTION_KEY, DEFAULT_SPLITTER_PROPORTION)
val splitter = createSplitter()
rootComponent.add(splitter, BorderLayout.CENTER)

// Left half
Expand All @@ -84,6 +82,14 @@ class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAU
// Load current state
reset()
templateTree.expandNodes()

// Select a scheme
initialSelection
?.let { currentTemplateList.getSchemeByUuid(it) }
?.also {
templateTree.selectedScheme = it
SwingUtilities.invokeLater { schemeEditor?.preferredFocusedComponent?.requestFocus() }
}
}

/**
Expand Down Expand Up @@ -125,6 +131,18 @@ class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAU
schemeEditorPanel.revalidate() // Show editor immediately
}

/**
* Creates a new splitter.
*
* If a test that depends on [TemplateListEditor] freezes for a long time or fails to initialize the UI, try
* setting [useTestSplitter] to `true`.
*
* @return the created splitter
*/
private fun createSplitter() =
if (useTestSplitter) JBSplitter(false, SPLITTER_PROPORTION_KEY, DEFAULT_SPLITTER_PROPORTION)
else OnePixelSplitter(false, SPLITTER_PROPORTION_KEY, DEFAULT_SPLITTER_PROPORTION)

/**
* Creates an editor to edit [scheme].
*/
Expand Down Expand Up @@ -174,13 +192,6 @@ class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAU
currentTemplateList.applyContext((+currentTemplateList.context).copy(templateList = currentTemplateList))

templateTree.reload()

queueSelection?.also {
templateTree.selectedScheme = currentTemplateList.getSchemeByUuid(it)
SwingUtilities.invokeLater { schemeEditor?.preferredFocusedComponent?.requestFocus() }

queueSelection = null
}
}

/**
Expand All @@ -200,8 +211,8 @@ class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAU
/**
* Scrolls to the newly focused element if that element is in the [editor].
*/
override fun propertyChange(event: PropertyChangeEvent?) {
val focused = event?.newValue as? JComponent
override fun propertyChange(event: PropertyChangeEvent) {
val focused = event.newValue as? JComponent
if (focused == null || !editor.rootComponent.isAncestorOf(focused))
return

Expand Down Expand Up @@ -245,14 +256,8 @@ class TemplateListEditor(val originalTemplateList: TemplateList = Settings.DEFAU
const val EDITOR_PANEL_MARGIN = 10

/**
* Creates a new splitter.
*
* For some reason, `OnePixelSplitter` does not work in tests. Therefore, tests overwrite this field to inject a
* different constructor.
* Whether [createSplitter] should use a separate kind of splitter that is more compatible with tests.
*/
var createSplitter: (Boolean, String, Float) -> Splitter =
{ vertical, proportionKey, defaultProportion ->
OnePixelSplitter(vertical, proportionKey, defaultProportion)
}
var useTestSplitter: Boolean = false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.fwdekker.randomness.template

import com.fwdekker.randomness.Settings
import com.fwdekker.randomness.testhelpers.afterNonContainer
import com.fwdekker.randomness.testhelpers.beforeNonContainer
import com.fwdekker.randomness.testhelpers.guiGet
import com.fwdekker.randomness.testhelpers.guiRun
import com.fwdekker.randomness.testhelpers.matchBundle
import com.fwdekker.randomness.testhelpers.shouldContainExactly
import com.intellij.openapi.options.ConfigurationException
import com.intellij.testFramework.fixtures.IdeaTestFixture
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.NamedTag
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.assertj.swing.edt.FailOnThreadViolationRepaintManager
import org.assertj.swing.fixture.Containers
import org.assertj.swing.fixture.FrameFixture


/**
* Unit tests for [TemplateListConfigurable].
*/
class TemplateListConfigurableTest : FunSpec({
tags(NamedTag("IdeaFixture"))


lateinit var ideaFixture: IdeaTestFixture
lateinit var frame: FrameFixture

lateinit var configurable: TemplateListConfigurable


beforeContainer {
FailOnThreadViolationRepaintManager.install()
TemplateListEditor.useTestSplitter = true
}

beforeNonContainer {
ideaFixture = IdeaTestFixtureFactory.getFixtureFactory().createBareFixture()
ideaFixture.setUp()

configurable = TemplateListConfigurable()
frame = Containers.showInFrame(guiGet { configurable.createComponent() })
}

afterNonContainer {
frame.cleanUp()
guiRun { configurable.disposeUIResources() }
ideaFixture.tearDown()
}


context("templateToSelect") {
test("selects the template with the given UUID") {
frame.cleanUp()
guiRun { configurable.disposeUIResources() }

configurable = TemplateListConfigurable()
configurable.schemeToSelect = Settings.DEFAULT.templates[2].uuid
frame = Containers.showInFrame(guiGet { configurable.createComponent() })

guiGet { frame.tree().target().selectionRows!! } shouldContainExactly arrayOf(4)
}
}


context("isModified") {
test("returns `false` if no modifications were made") {
configurable.isModified shouldBe false
}

test("returns `true` if modifications were made") {
guiRun { frame.textBox("templateName").target().text = "New Name" }

configurable.isModified shouldBe true
}

test("returns `true` if no modifications were made but the template list is invalid") {
Settings.DEFAULT.templates[0].name = ""
guiRun { configurable.reset() }

configurable.editor.isModified() shouldBe false
configurable.isModified shouldBe true
}
}

context("apply") {
test("throws an exception if the template list is invalid") {
guiRun { frame.textBox("templateName").target().text = "" }

shouldThrow<ConfigurationException> { configurable.apply() }
.title should matchBundle("template_list.error.failed_to_save_settings")
}

test("applies the changes") {
guiRun { frame.textBox("templateName").target().text = "New Name" }

configurable.apply()

Settings.DEFAULT.templates[0].name shouldBe "New Name"
}
}

context("reset") {
test("resets the editor") {
guiRun { frame.textBox("templateName").target().text = "Changed Name" }

guiRun { configurable.reset() }

guiGet { frame.textBox("templateName").target().text } shouldNotBe "Changed Name"
}
}
})
Loading

0 comments on commit 0af6ce3

Please sign in to comment.