diff --git a/intellij-plugin/build.gradle.kts b/intellij-plugin/build.gradle.kts index 21200b939..d78054a3f 100644 --- a/intellij-plugin/build.gradle.kts +++ b/intellij-plugin/build.gradle.kts @@ -167,10 +167,6 @@ tasks { } withType { - from("socialMedia") { - into("${projectName.get()}/socialMedia") - include("**/*.gif") - } doLast { val kotlinJarRe = """kotlin-(stdlib|reflect|runtime).*\.jar""".toRegex() val libraryDir = destinationDir.resolve("${projectName.get()}/lib") diff --git a/intellij-plugin/hs-core/resources/META-INF/Hyperskill.xml b/intellij-plugin/hs-core/resources/META-INF/Hyperskill.xml index a008cde8a..7f5cbb5e2 100644 --- a/intellij-plugin/hs-core/resources/META-INF/Hyperskill.xml +++ b/intellij-plugin/hs-core/resources/META-INF/Hyperskill.xml @@ -29,7 +29,9 @@ + + diff --git a/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties b/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties index 41ab28e79..70bc6bc4f 100644 --- a/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties +++ b/intellij-plugin/hs-core/resources/messages/EduCoreBundle.properties @@ -619,3 +619,16 @@ yaml.editor.notification.parameter.is.empty={0} is empty # {0} for course YAML version, {1} for supported YAML version yaml.version.compatibility.title=Course Created with Newer Plugin Version yaml.version.compatibility.message=This course was created with a newer version of the plugin (YAML version {0}). The current plugin supports YAML version {1}. The course will be loaded in compatibility mode, and the YAML version will be automatically downgraded. Some features from the newer plugin version may not be available. + +# Suggest to share achievement on project completion +social.media.suggest.to.post.dialog.title=Congratulations! +social.media.share.on.x.button.text=Share on X +social.media.share.on.linkedin.button.text=Share on LinkedIn +social.media.close.button.text=Close +social.media.do.not.ask.dialog.checkbox=Don't ask again +# {0} for the project name +social.media.hyperskill.achievement.message=#HyperskillAchievement unlocked! I''ve just programmed the "{0}" application on Hyperskill! +# {0} for the link (a bare domain in the dialog, a full tracked URL in the LinkedIn share text) +social.media.learn.more.at=Learn more at {0} +social.media.settings.prompt.to.share=Suggest sharing achievements after completing a project +social.media.settings.display.name=Social Media diff --git a/intellij-plugin/hs-core/resources/socialMedia/hyperskill/[email protected] b/intellij-plugin/hs-core/resources/socialMedia/hyperskill/[email protected] new file mode 100644 index 000000000..e6bc672d1 Binary files /dev/null and b/intellij-plugin/hs-core/resources/socialMedia/hyperskill/[email protected] differ diff --git a/intellij-plugin/hs-core/resources/socialMedia/hyperskill/project_complete.png b/intellij-plugin/hs-core/resources/socialMedia/hyperskill/project_complete.png new file mode 100644 index 000000000..98819d4f2 Binary files /dev/null and b/intellij-plugin/hs-core/resources/socialMedia/hyperskill/project_complete.png differ diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaOptionsProvider.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaOptionsProvider.kt new file mode 100644 index 000000000..aea248e6d --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaOptionsProvider.kt @@ -0,0 +1,34 @@ +package org.hyperskill.academy.socialMedia + +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.dsl.builder.panel +import org.hyperskill.academy.learning.messages.EduCoreBundle +import org.hyperskill.academy.learning.settings.OptionsProvider +import javax.swing.JComponent + +/** + * Settings checkbox that lets the user re-enable the "share your achievement" suggestion + * after it was turned off via the "Don't ask again" checkbox. + */ +class SocialMediaOptionsProvider : OptionsProvider { + + private val askToPostCheckBox = JBCheckBox(EduCoreBundle.message("social.media.settings.prompt.to.share")) + + override fun createComponent(): JComponent = panel { + row { + cell(askToPostCheckBox) + } + } + + override fun isModified(): Boolean = askToPostCheckBox.isSelected != SocialMediaSettings.getInstance().askToPost + + override fun apply() { + SocialMediaSettings.getInstance().askToPost = askToPostCheckBox.isSelected + } + + override fun reset() { + askToPostCheckBox.isSelected = SocialMediaSettings.getInstance().askToPost + } + + override fun getDisplayName(): String = EduCoreBundle.message("social.media.settings.display.name") +} diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaSettings.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaSettings.kt new file mode 100644 index 000000000..05c993230 --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaSettings.kt @@ -0,0 +1,39 @@ +package org.hyperskill.academy.socialMedia + +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import org.hyperskill.academy.learning.EduTestAware +import org.hyperskill.academy.learning.isUnitTestMode + +/** + * Stores the user preference whether to show the "share your achievement" dialog + * on Hyperskill project completion. Disabled permanently once the user checks "Don't ask again". + */ +@Service(Service.Level.APP) +@State(name = "HyperskillSocialMediaSettings", storages = [Storage("hyperskill.xml")]) +class SocialMediaSettings : SimplePersistentStateComponent(SocialMediaState()), EduTestAware { + + // Don't use property delegation like `var askToPost by state::askToPost`. + // It doesn't work because `state` may change but delegation keeps the initial state object + var askToPost: Boolean + get() = state.askToPost + set(value) { + state.askToPost = value + } + + override fun cleanUpState() { + askToPost = !isUnitTestMode + } + + companion object { + fun getInstance(): SocialMediaSettings = service() + } + + class SocialMediaState : BaseState() { + var askToPost: Boolean by property(!isUnitTestMode) + } +} diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaUtils.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaUtils.kt new file mode 100644 index 000000000..ecc47e746 --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SocialMediaUtils.kt @@ -0,0 +1,80 @@ +package org.hyperskill.academy.socialMedia + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.IconLoader +import org.hyperskill.academy.learning.course +import org.hyperskill.academy.learning.courseFormat.CheckStatus +import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillCourse +import org.hyperskill.academy.learning.courseFormat.tasks.Task +import org.hyperskill.academy.learning.messages.EduCoreBundle +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import javax.swing.Icon + +object SocialMediaUtils { + + /** + * Hyperskill courses page the achievement points to. UTM tags are part of the agreed link and must be kept as is. + * Used both as the `url` parameter of the X share intent and embedded into the LinkedIn share text. + */ + const val SHARE_URL: String = + "https://hyperskill.org/courses?source=ide_share&utm_source=jetbrains&utm_medium=social&utm_campaign=ide_plugin" + + // Human-friendly link shown in the dialog body (without tracking parameters) + private const val COURSES_DISPLAY_LINK: String = "hyperskill.org/courses" + + private const val ACHIEVEMENT_IMAGE_PATH = "/socialMedia/hyperskill/project_complete.png" + + /** + * Defines the policy when the user is suggested to share the achievement. + * The dialog is shown only once the whole Hyperskill project (the project lesson) is solved + * and only right after the task transitions to the solved state (not on re-solving an already solved task). + */ + fun shouldSuggestToPost(project: Project, solvedTask: Task, statusBeforeCheck: CheckStatus): Boolean { + val course = project.course as? HyperskillCourse ?: return false + if (!course.isStudy) return false + if (statusBeforeCheck == CheckStatus.Solved) return false + + val projectLesson = course.getProjectLesson() ?: return false + if (solvedTask.lesson != projectLesson) return false + + var allProjectTasksSolved = true + projectLesson.visitTasks { + allProjectTasksSolved = allProjectTasksSolved && it.status == CheckStatus.Solved + } + return allProjectTasksSolved + } + + /** The achievement sentence without any link. Shown as the base of the dialog message and used as the X post text. */ + private fun achievementCore(solvedTask: Task): String { + val course = solvedTask.course + val projectName = (course as? HyperskillCourse)?.getProjectLesson()?.presentableName ?: course.presentableName + return EduCoreBundle.message("social.media.hyperskill.achievement.message", projectName) + } + + /** Text shown (and selectable) in the dialog body: the achievement plus the human-friendly courses link. */ + fun getDisplayMessage(solvedTask: Task): String = + "${achievementCore(solvedTask)} ${EduCoreBundle.message("social.media.learn.more.at", COURSES_DISPLAY_LINK)}" + + /** + * X share intent. The post text intentionally has no link because X renders the [SHARE_URL] from the `url` parameter. + * https://twitter.com/intent/tweet?text=...&url=... + */ + fun buildXShareUrl(solvedTask: Task): String { + val text = achievementCore(solvedTask) + return "https://twitter.com/intent/tweet?text=${encode(text)}&url=${encode(SHARE_URL)}" + } + + /** + * LinkedIn share intent. LinkedIn has no separate `url` parameter, so the tracked [SHARE_URL] is embedded into the text. + * https://www.linkedin.com/feed/?shareActive=true&text=... + */ + fun buildLinkedInShareUrl(solvedTask: Task): String { + val text = "${achievementCore(solvedTask)} ${EduCoreBundle.message("social.media.learn.more.at", SHARE_URL)}" + return "https://www.linkedin.com/feed/?shareActive=true&text=${encode(text)}" + } + + fun loadAchievementImage(): Icon? = IconLoader.findIcon(ACHIEVEMENT_IMAGE_PATH, SocialMediaUtils::class.java.classLoader) + + private fun encode(value: String): String = URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20") +} diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SuggestToPostOnProjectCompletionListener.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SuggestToPostOnProjectCompletionListener.kt new file mode 100644 index 000000000..813e19b63 --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/SuggestToPostOnProjectCompletionListener.kt @@ -0,0 +1,36 @@ +package org.hyperskill.academy.socialMedia + +import com.intellij.openapi.project.Project +import org.hyperskill.academy.learning.checker.CheckListener +import org.hyperskill.academy.learning.courseFormat.CheckResult +import org.hyperskill.academy.learning.courseFormat.CheckStatus +import org.hyperskill.academy.learning.courseFormat.tasks.Task +import org.hyperskill.academy.socialMedia.suggestToPostDialog.createSuggestToPostDialogUI + +/** + * Suggests sharing the achievement once a Hyperskill project is fully solved. + * See [SocialMediaUtils.shouldSuggestToPost] for the exact policy. + */ +class SuggestToPostOnProjectCompletionListener : CheckListener { + + private var statusBeforeCheck: CheckStatus? = null + + override fun beforeCheck(project: Project, task: Task) { + statusBeforeCheck = task.status + } + + override fun afterCheck(project: Project, task: Task, result: CheckResult) { + val statusBefore = statusBeforeCheck ?: return + statusBeforeCheck = null + + if (!SocialMediaSettings.getInstance().askToPost) return + if (!SocialMediaUtils.shouldSuggestToPost(project, task, statusBefore)) return + + createSuggestToPostDialogUI( + project, + SocialMediaUtils.getDisplayMessage(task), + SocialMediaUtils.buildXShareUrl(task), + SocialMediaUtils.buildLinkedInShareUrl(task) + ).showAndGet() + } +} diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialog.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialog.kt new file mode 100644 index 000000000..9a3833752 --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialog.kt @@ -0,0 +1,72 @@ +package org.hyperskill.academy.socialMedia.suggestToPostDialog + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.DoNotAskOption +import org.hyperskill.academy.learning.EduBrowser +import org.hyperskill.academy.learning.messages.EduCoreBundle +import org.hyperskill.academy.socialMedia.SocialMediaSettings +import org.hyperskill.academy.socialMedia.SocialMediaUtils +import java.awt.event.ActionEvent +import javax.swing.AbstractAction +import javax.swing.Action +import javax.swing.JComponent + +/** + * Modal dialog suggesting to share the Hyperskill project achievement. + * Provides "Share on X" and "Share on LinkedIn" buttons that open the corresponding web share intents, + * a "Close" button, and a "Don't ask again" checkbox that disables the suggestion via [SocialMediaSettings]. + */ +class SuggestToPostDialog( + project: Project, + message: String, + private val xShareUrl: String, + private val linkedInShareUrl: String +) : SuggestToPostDialogUI, DialogWrapper(project) { + + private val panel = SuggestToPostDialogPanel(message, SocialMediaUtils.loadAchievementImage()) + + init { + title = EduCoreBundle.message("social.media.suggest.to.post.dialog.title") + setCancelButtonText(EduCoreBundle.message("social.media.close.button.text")) + isResizable = false + installDoNotAskOption() + init() + } + + // `setDoNotAskOption` is the standard way to add a "do not ask again" checkbox to a custom `DialogWrapper`. + // It is deprecated in the platform but kept intentionally as it works across all supported versions. + @Suppress("DEPRECATION") + private fun installDoNotAskOption() { + setDoNotAskOption(DoNotAskToSuggestOption()) + } + + override fun createCenterPanel(): JComponent = panel + + // Share buttons open a browser but keep the dialog open so the user can share to both networks + override fun createActions(): Array = arrayOf( + ShareAction(EduCoreBundle.message("social.media.share.on.x.button.text"), xShareUrl), + ShareAction(EduCoreBundle.message("social.media.share.on.linkedin.button.text"), linkedInShareUrl), + cancelAction + ) + + private class ShareAction(name: String, private val url: String) : AbstractAction(name) { + override fun actionPerformed(e: ActionEvent?) { + EduBrowser.getInstance().browse(url) + } + } + + private class DoNotAskToSuggestOption : DoNotAskOption.Adapter() { + override fun rememberChoice(isSelected: Boolean, exitCode: Int) { + // `isSelected` == true means the user checked "Don't ask again" and wants to opt out + if (isSelected) { + SocialMediaSettings.getInstance().askToPost = false + } + } + + // Persist the opt-out even if the dialog is closed via the "Close" (cancel) button + override fun shouldSaveOptionsOnCancel(): Boolean = true + + override fun getDoNotShowMessage(): String = EduCoreBundle.message("social.media.do.not.ask.dialog.checkbox") + } +} diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialogPanel.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialogPanel.kt new file mode 100644 index 000000000..6cef28f68 --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialogPanel.kt @@ -0,0 +1,54 @@ +package org.hyperskill.academy.socialMedia.suggestToPostDialog + +import com.intellij.openapi.ui.VerticalFlowLayout +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.JBUI +import java.awt.Dimension +import javax.swing.Box +import javax.swing.Icon +import javax.swing.JPanel + +/** + * Center panel of the [SuggestToPostDialog]: a selectable (so it can be copied) achievement message + * followed by an optional banner image. + */ +class SuggestToPostDialogPanel(message: String, image: Icon?) : JPanel(VerticalFlowLayout(0, 0)) { + + init { + border = JBUI.Borders.empty() + + val width = image?.iconWidth ?: JBUI.scale(DEFAULT_WIDTH) + add(MessageTextArea(message, width)) + + if (image != null) { + // Don't use a border for the label because it changes the size of its content + add(Box.createVerticalStrut(JBUI.scale(10))) + add(JBLabel(image)) + } + } + + /** + * Read-only, word-wrapped text area whose preferred width is pinned to [targetWidth] + * so the dialog wraps the message to the banner width instead of stretching to a single long line. + */ + private class MessageTextArea(text: String, private val targetWidth: Int) : JBTextArea(text) { + init { + lineWrap = true + wrapStyleWord = true + isEditable = false + isOpaque = false + border = JBUI.Borders.empty() + } + + override fun getPreferredSize(): Dimension { + // Constrain the width first so the height is computed for the wrapped layout + setSize(targetWidth, Short.MAX_VALUE.toInt()) + return Dimension(targetWidth, super.getPreferredSize().height) + } + } + + companion object { + private const val DEFAULT_WIDTH = 600 + } +} diff --git a/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialogUI.kt b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialogUI.kt new file mode 100644 index 000000000..df70b24f1 --- /dev/null +++ b/intellij-plugin/hs-core/src/org/hyperskill/academy/socialMedia/suggestToPostDialog/SuggestToPostDialogUI.kt @@ -0,0 +1,40 @@ +package org.hyperskill.academy.socialMedia.suggestToPostDialog + +import com.intellij.openapi.project.Project +import org.hyperskill.academy.learning.isUnitTestMode +import org.jetbrains.annotations.TestOnly + +interface SuggestToPostDialogUI { + /** + * Shows the dialog. Returns `true` only if it was closed via an OK action. + * Sharing happens through in-dialog actions, so callers don't rely on the return value. + */ + fun showAndGet(): Boolean +} + +fun createSuggestToPostDialogUI( + project: Project, + message: String, + xShareUrl: String, + linkedInShareUrl: String +): SuggestToPostDialogUI { + return if (isUnitTestMode) { + MOCK ?: error("You should set mock UI via `withMockSuggestToPostDialogUI`") + } + else { + SuggestToPostDialog(project, message, xShareUrl, linkedInShareUrl) + } +} + +private var MOCK: SuggestToPostDialogUI? = null + +@TestOnly +fun withMockSuggestToPostDialogUI(mockUI: SuggestToPostDialogUI, action: () -> Unit) { + try { + MOCK = mockUI + action() + } + finally { + MOCK = null + } +} diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/EduTestServiceStateHelper.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/EduTestServiceStateHelper.kt index 70b5a19c2..73aef119c 100644 --- a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/EduTestServiceStateHelper.kt +++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/learning/EduTestServiceStateHelper.kt @@ -210,7 +210,7 @@ private class HardcodedServiceCollector : TestAwareServiceCollector { "org.hyperskill.academy.learning.agreement.UserAgreementSettings", "org.hyperskill.academy.learning.newproject.coursesStorage.CoursesStorage", "org.hyperskill.academy.learning.stepik.hyperskill.metrics.HyperskillMetricsService", - "org.hyperskill.academy.socialMedia.x.XSettings", + "org.hyperskill.academy.socialMedia.SocialMediaSettings", ) private val projectServices = listOf( diff --git a/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/socialMedia/SuggestToPostOnProjectCompletionListenerTest.kt b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/socialMedia/SuggestToPostOnProjectCompletionListenerTest.kt new file mode 100644 index 000000000..d7a3c5bdd --- /dev/null +++ b/intellij-plugin/hs-core/testSrc/org/hyperskill/academy/socialMedia/SuggestToPostOnProjectCompletionListenerTest.kt @@ -0,0 +1,131 @@ +package org.hyperskill.academy.socialMedia + +import org.hyperskill.academy.learning.EduTestCase +import org.hyperskill.academy.learning.checker.CheckListener +import org.hyperskill.academy.learning.courseFormat.CheckResult +import org.hyperskill.academy.learning.courseFormat.CheckStatus +import org.hyperskill.academy.learning.courseFormat.EduFormatNames.HYPERSKILL_TOPICS +import org.hyperskill.academy.learning.courseFormat.hyperskill.HyperskillCourse +import org.hyperskill.academy.learning.courseFormat.tasks.Task +import org.hyperskill.academy.learning.stepik.hyperskill.hyperskillCourseWithFiles +import org.hyperskill.academy.socialMedia.suggestToPostDialog.SuggestToPostDialogUI +import org.hyperskill.academy.socialMedia.suggestToPostDialog.withMockSuggestToPostDialogUI +import org.junit.Test + +class SuggestToPostOnProjectCompletionListenerTest : EduTestCase() { + + override fun setUp() { + super.setUp() + SocialMediaSettings.getInstance().askToPost = true + } + + override fun tearDown() { + try { + SocialMediaSettings.getInstance().askToPost = false + } + catch (e: Throwable) { + addSuppressedException(e) + } + finally { + super.tearDown() + } + } + + @Test + fun `test show dialog after last project task solved`() { + val course = createHyperskillCourse() + val projectLesson = course.getProjectLesson() ?: error("No project lesson") + val lastTask = projectLesson.getTask("Task2") ?: error("No Task2") + // All other project tasks are already solved before the last one is checked + projectLesson.visitTasks { if (it != lastTask) it.status = CheckStatus.Solved } + + val shown = simulateCheck(lastTask) { it.status = CheckStatus.Solved } + assertTrue("Dialog should be shown after the whole project is solved", shown) + } + + @Test + fun `test do not show dialog if not all project tasks solved`() { + val course = createHyperskillCourse() + val lastTask = course.getProjectLesson()?.getTask("Task2") ?: error("No Task2") + // `Task1` stays unchecked + + val shown = simulateCheck(lastTask) { it.status = CheckStatus.Solved } + assertFalse("Dialog should not be shown until the whole project is solved", shown) + } + + @Test + fun `test do not show dialog if solved task is not a project task`() { + val course = createHyperskillCourse() + val codeTask = course.getLesson(HYPERSKILL_TOPICS, TOPIC_NAME)?.getTask("CodeTask") ?: error("No CodeTask") + + val shown = simulateCheck(codeTask) { it.status = CheckStatus.Solved } + assertFalse("Dialog should not be shown for tasks outside the project lesson", shown) + } + + @Test + fun `test do not show dialog if project task solved again`() { + val course = createHyperskillCourse() + val projectLesson = course.getProjectLesson() ?: error("No project lesson") + projectLesson.visitTasks { it.status = CheckStatus.Solved } + val lastTask = projectLesson.getTask("Task2") ?: error("No Task2") + + // The task is already solved before the check, so re-solving must not show the dialog + val shown = simulateCheck(lastTask) { it.status = CheckStatus.Solved } + assertFalse("Dialog should not be shown when an already solved task is re-checked", shown) + } + + @Test + fun `test do not show dialog if disabled in settings`() { + SocialMediaSettings.getInstance().askToPost = false + val course = createHyperskillCourse() + val projectLesson = course.getProjectLesson() ?: error("No project lesson") + val lastTask = projectLesson.getTask("Task2") ?: error("No Task2") + projectLesson.visitTasks { if (it != lastTask) it.status = CheckStatus.Solved } + + val shown = simulateCheck(lastTask) { it.status = CheckStatus.Solved } + assertFalse("Dialog should not be shown when the suggestion is disabled in settings", shown) + } + + /** + * Drives the real (EP-registered) listener through a check of [task]: + * captures the status before the check, applies [solve], then fires `afterCheck`. + * Returns whether the suggestion dialog would be shown. + */ + private fun simulateCheck(task: Task, solve: (Task) -> Unit): Boolean { + val listener = CheckListener.EP_NAME.findExtensionOrFail(SuggestToPostOnProjectCompletionListener::class.java) + var shown = false + withMockSuggestToPostDialogUI(object : SuggestToPostDialogUI { + override fun showAndGet(): Boolean { + shown = true + return false + } + }) { + listener.beforeCheck(project, task) + solve(task) + listener.afterCheck(project, task, CheckResult.SOLVED) + } + return shown + } + + private fun createHyperskillCourse(): HyperskillCourse = hyperskillCourseWithFiles { + frameworkLesson("Project") { + eduTask("Task1") { + taskFile("task.txt") + } + eduTask("Task2") { + taskFile("task.txt") + } + } + section(HYPERSKILL_TOPICS) { + lesson(TOPIC_NAME) { + codeTask("CodeTask") { + taskFile("task.txt") + } + } + } + } + + companion object { + private const val TOPIC_NAME = "topicName" + } +}