blob: a8b41d45213640fddd85bdb71ffc4fcfe8b68676 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.assistant.view
import com.android.tools.idea.assistant.TutorialCardRefreshNotifier
import com.android.tools.idea.assistant.datamodel.AnalyticsProvider
import com.android.tools.idea.assistant.datamodel.FeatureData
import com.android.tools.idea.assistant.datamodel.StepData
import com.android.tools.idea.assistant.datamodel.StepElementData
import com.android.tools.idea.assistant.datamodel.TutorialBundleData
import com.android.tools.idea.assistant.datamodel.TutorialData
import com.google.common.truth.Truth.assertThat
import com.intellij.testFramework.ProjectRule
import com.intellij.ui.components.JBScrollPane
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import javax.swing.BorderFactory
import javax.swing.Icon
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.border.Border
import org.junit.Rule
import org.junit.Test
internal class TutorialCardTest {
@get:Rule val projectRule = ProjectRule()
@Test
fun `null scroll pane is not expected`() {
val card = createTestTutorialCard()
assertThat(card.isViewportExpectedLayout(null)).isFalse()
}
@Test
fun `empty viewport is not expected`() {
val card = createTestTutorialCard()
var scrollPane = JBScrollPane()
assertThat(card.isViewportExpectedLayout(scrollPane)).isFalse()
}
@Test
fun `first component in viewport should be a JPanel`() {
val card = createTestTutorialCard()
var scrollPane = JBScrollPane()
scrollPane.viewport.add(JLabel())
assertThat(card.isViewportExpectedLayout(scrollPane)).isFalse()
}
@Test
fun `expected viewport is not null and first component is a JPanel`() {
val card = createTestTutorialCard()
var scrollPane = JBScrollPane()
scrollPane.viewport.add(JPanel())
card.add(scrollPane)
assertThat(card.isViewportExpectedLayout(scrollPane)).isTrue()
}
@Test
fun `only tutorial cards return correct scroll position`() {
val card = createTestTutorialCard()
// Given a tutorialCard with an expected layout
var scrollPane = JBScrollPane()
val innerJPanel = JPanel()
val tutorialCardCount = 2
addTutorialSteps(innerJPanel, tutorialCardCount)
scrollPane.viewport.add(innerJPanel, tutorialCardCount)
// When getting the last tutorialCard scroll position
val scrollbarPositon = card.getVerticalScrollbarPosition(tutorialCardCount, scrollPane)
// Then the position is set to the last tutorialCards scroll position
assertThat(scrollbarPositon).isEqualTo(2)
}
@Test
fun `mix of components return appropriate tutorial card scroll position`() {
// Given tutorialCard UI setup with a JPanel and tutorial cards
val card = createTestTutorialCard()
var scrollPane = JBScrollPane()
val innerJPanel = JPanel()
val tutorialCardCount = 2
innerJPanel.add(JPanel())
addTutorialSteps(innerJPanel, tutorialCardCount)
scrollPane.viewport.add(innerJPanel, tutorialCardCount)
innerJPanel.add(JPanel())
// When getting the last tutorialCard scroll position
val scrollbarPosition = card.getVerticalScrollbarPosition(tutorialCardCount, scrollPane)
// Then the position is set to the last tutorialCards scroll position
assertThat(scrollbarPosition).isEqualTo(2)
}
@Test
fun `tutorial step that doesn't exist returns zero for scroll position`() {
val card = createTestTutorialCard()
// Given tutorialCard UI setup
var scrollPane = JBScrollPane()
val innerJPanel = JPanel()
val tutorialCardCount = 2
innerJPanel.add(JPanel())
addTutorialSteps(innerJPanel, tutorialCardCount)
scrollPane.viewport.add(innerJPanel, tutorialCardCount)
// When getting the last tutorialCard scroll position
val scrollbarPosition = card.getVerticalScrollbarPosition(tutorialCardCount + 1, scrollPane)
// Then the position is set to the last tutorialCards scroll position
assertThat(scrollbarPosition).isEqualTo(0)
}
private fun addTutorialSteps(innerJPanel: JPanel, tutorialCardCount: Int) {
for (i in 0..tutorialCardCount) {
val step = addTutorialStep(i)
innerJPanel.add(step)
}
}
// using height here and setting the bounds to it to have an easier way to do validation without
// rendering the whole UI
private fun addTutorialStep(yIndex: Int): TutorialStep {
val listener = TestActionListener()
val step =
TutorialStep(
TestStepData(),
yIndex,
listener,
projectRule.project,
true,
false,
TutorialStep::class.java,
)
// only y bounds matter for scroll wheel setting
step.setBounds(0, yIndex, 0, 0)
return step
}
@Test
fun `tutorial card grabs tutorial steps from its bundle`() {
// Given the bundle to make a tutorial card
var testTutorialData = TestTutorialData()
var testTutorialBundleData = TestTutorialBundleData()
var myFeaturesPanel =
FeaturesPanel(testTutorialBundleData, projectRule.project, TestAnalyticsProvider(), null)
// When creating a tutorial card
TutorialCard(myFeaturesPanel, testTutorialData, TestFeatureData(), true, testTutorialBundleData)
// Then
assertThat(testTutorialData.myGetStepCounter).isEqualTo(1)
}
@Test
fun `tutorial card redraws when receiving a message on refresh topic`() {
// Given a tutorial card
var testTutorialData = TestTutorialData()
var testTutorialBundleData = TestTutorialBundleData()
var myFeaturesPanel =
FeaturesPanel(testTutorialBundleData, projectRule.project, TestAnalyticsProvider(), null)
val card =
TutorialCard(
myFeaturesPanel,
testTutorialData,
TestFeatureData(),
true,
testTutorialBundleData,
)
// normally called by swing internals during setup
card.addNotify()
assertThat(testTutorialData.myGetStepCounter).isEqualTo(1)
// When a message is sent on the refresh tutorial card topic
sendMessage()
// Then internal counter is 2
assertThat(testTutorialData.myGetStepCounter).isEqualTo(2)
}
@Test
fun `tutorial card that is not subscribed does not receive messages`() {
// Given a tutorial card that is not notified of messages
var testTutorialData = TestTutorialData()
var testTutorialBundleData = TestTutorialBundleData()
var myFeaturesPanel =
FeaturesPanel(testTutorialBundleData, projectRule.project, TestAnalyticsProvider(), null)
val card =
TutorialCard(
myFeaturesPanel,
testTutorialData,
TestFeatureData(),
true,
testTutorialBundleData,
)
assertThat(testTutorialData.myGetStepCounter).isEqualTo(1)
// called on close by swing internals
card.removeNotify()
// when a message is sent
sendMessage()
// Then the internal counter is still 1
assertThat(testTutorialData.myGetStepCounter).isEqualTo(1)
}
@Test
fun `tutorial card can resubscribe and receive events`() {
// Given a tutorial card that is not notified of messages
var testTutorialData = TestTutorialData()
var testTutorialBundleData = TestTutorialBundleData()
var myFeaturesPanel =
FeaturesPanel(testTutorialBundleData, projectRule.project, TestAnalyticsProvider(), null)
val card =
TutorialCard(
myFeaturesPanel,
testTutorialData,
TestFeatureData(),
true,
testTutorialBundleData,
)
card.addNotify()
assertThat(testTutorialData.myGetStepCounter).isEqualTo(1)
sendMessage()
assertThat(testTutorialData.myGetStepCounter).isEqualTo(2)
card.removeNotify()
sendMessage()
assertThat(testTutorialData.myGetStepCounter).isEqualTo(2)
card.addNotify()
sendMessage()
assertThat(testTutorialData.myGetStepCounter).isEqualTo(3)
}
private fun sendMessage(): Unit =
projectRule.project.messageBus
.syncPublisher(TutorialCardRefreshNotifier.TUTORIAL_CARD_TOPIC)
.stateUpdated(1)
private fun createTestTutorialCard(): TutorialCard {
var testTutorialData = TestTutorialData()
var testTutorialBundleData = TestTutorialBundleData()
var myFeaturesPanel =
FeaturesPanel(testTutorialBundleData, projectRule.project, TestAnalyticsProvider(), null)
return TutorialCard(
myFeaturesPanel,
testTutorialData,
TestFeatureData(),
true,
testTutorialBundleData,
)
}
class TestActionListener : ActionListener {
override fun actionPerformed(e: ActionEvent) {}
}
// Classes required for running tests
class TestAnalyticsProvider : AnalyticsProvider
class TestTutorialBundleData : TutorialBundleData {
override fun setResourceClass(clazz: Class<*>) {}
override fun getName(): String = "myTutorialBundleDataName"
override fun getIcon(): Icon? = null
override fun getLogo(): Icon? = null
override fun getFeatures(): List<FeatureData> {
return listOf()
}
override fun getWelcome(): String = "TutorialBundleDataWelcome"
override fun getBundleCreatorId(): String = "myBundleCreatorId"
override fun setBundleCreatorId(bundleCreatorId: String) {
this.bundleCreatorId = bundleCreatorId
}
override fun isStepByStep(): Boolean = false
override fun hideStepIndex(): Boolean = false
}
class TestTutorialData : TutorialData {
// using a stepCounter to keep track of calls to getSteps() easily
var myGetStepCounter = 0
override fun getLabel(): String = "MyTutorialDataLabel"
override fun getDescription(): String = "MyTutorialData"
override fun getKey(): String {
return "myTutorialDataKey"
}
override fun getSteps(): List<StepData?> {
myGetStepCounter++
return listOf(TestStepData(), TestStepData())
}
override fun hasLocalHTMLPaths() = false
override fun shouldLoadLazily() = false
override fun getResourceClass(): Class<*> {
return TutorialData::class.java
}
override fun getRemoteLink(): String? = null
override fun getRemoteLinkLabel(): String? = null
override fun getIcon(): Icon? = null
}
class TestFeatureData : FeatureData {
override fun getName(): String = "myFeatureDataName"
override fun getIcon(): Icon? = null
override fun getDescription(): String = "myFeatureDataDescription"
override fun getTutorials(): List<TutorialData> = listOf()
override fun setResourceClass(clazz: Class<*>) {
this.setResourceClass(clazz)
}
}
class TestStepData : StepData {
override fun getStepElements(): MutableList<StepElementData> {
return mutableListOf()
}
override fun getLabel(): String = "StepDataLabel"
override fun getBorder(): Border = BorderFactory.createEmptyBorder()
}
}