mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[terminal] IJPL-252435 Add more end-to-end tests for OSC8 hyperlinks
(cherry picked from commit d0cb7d941b74b23105d7ab9cd2d90f8088e3c170) IJ-CR-217674 GitOrigin-RevId: 77f3d7424983e4c2712e593f22126ce27722eca7
This commit is contained in:
committed by
intellij-monorepo-bot
parent
06b4d9d535
commit
be24d5d204
+77
-7
@@ -19,9 +19,11 @@ import kotlinx.coroutines.withContext
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.jetbrains.plugins.terminal.JBTerminalSystemSettingsProvider
|
||||
import org.jetbrains.plugins.terminal.util.terminalProjectScope
|
||||
import org.jetbrains.plugins.terminal.view.TerminalOutputModel
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import java.awt.event.MouseEvent
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -37,11 +39,54 @@ internal class TerminalOsc8HyperlinksEndToEndTest : BasePlatformTestCase() {
|
||||
override fun runInDispatchThread(): Boolean = false
|
||||
|
||||
@Test
|
||||
fun `OSC8 hyperlink is rendered as a hyperlink highlighter in the editor markup model`(): Unit = doTest { fixture ->
|
||||
fun `OSC8 hyperlink text can differ from its target URI`(): Unit = doTest { fixture ->
|
||||
fixture.connector.feed("before ${osc8("https://example.com", "link text")} after")
|
||||
|
||||
val highlighter = fixture.awaitHyperlinkHighlighter()
|
||||
val highlighter = fixture.awaitHyperlink()
|
||||
assertThat(fixture.textOf(highlighter)).isEqualTo("link text")
|
||||
assertThat(fixture.uriOf(highlighter)).isEqualTo("https://example.com")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `several OSC8 hyperlinks in the same output are each rendered separately`(): Unit = doTest { fixture ->
|
||||
fixture.connector.feed("${osc8("https://jetbrains.com", "FIRST")} middle ${osc8("https://example.com", "SECOND")}")
|
||||
|
||||
val highlighters = fixture.awaitHyperlinks(2)
|
||||
assertThat(highlighters.map { fixture.textOf(it) }).containsExactly("FIRST", "SECOND")
|
||||
assertThat(highlighters.map { fixture.uriOf(it) }).containsExactly("https://jetbrains.com", "https://example.com")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a target that is not a recognized URL is not rendered as a hyperlink`(): Unit = doTest { fixture ->
|
||||
fixture.connector.feed(osc8("definitely-not-a-url", "click me"))
|
||||
// A real link fed right after: once it's rendered, reconciliation has scanned the whole link list
|
||||
// (including the one above) at least once, so a missing highlighter for "click me" isn't just a timing fluke.
|
||||
fixture.connector.feed(osc8("https://jetbrains.com", "SENTINEL"))
|
||||
|
||||
val highlighters = fixture.awaitHyperlinks(1)
|
||||
assertThat(fixture.textOf(highlighters.single())).isEqualTo("SENTINEL")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hyperlink is removed from the markup model once its line is overwritten with plain text`(): Unit = doTest { fixture ->
|
||||
fixture.connector.feed(osc8("https://jetbrains.com", "LINK"))
|
||||
fixture.awaitHyperlink()
|
||||
|
||||
// Move the cursor back to the start of the line and overwrite it with plain text at least as long as "LINK".
|
||||
fixture.connector.feed("\rplain text, no links here")
|
||||
|
||||
fixture.awaitHyperlinks(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hovering an OSC8 hyperlink shows its target URI as a tooltip`(): Unit = doTest { fixture ->
|
||||
fixture.connector.feed("x ${osc8("https://jetbrains.com", "JB")} y")
|
||||
|
||||
val highlighter = fixture.awaitHyperlink()
|
||||
fixture.hover(highlighter)
|
||||
|
||||
// The link text ("JB") says nothing about the destination - the tooltip is what reveals it.
|
||||
assertThat(fixture.editor.contentComponent.toolTipText).contains("https://jetbrains.com")
|
||||
}
|
||||
|
||||
private fun doTest(test: suspend (Fixture) -> Unit) {
|
||||
@@ -70,6 +115,8 @@ internal class TerminalOsc8HyperlinksEndToEndTest : BasePlatformTestCase() {
|
||||
val terminalView = TerminalViewImpl(project, JBTerminalSystemSettingsProvider(), null, scope)
|
||||
terminalView.connectToSession(session)
|
||||
editor = terminalView.outputEditor as EditorImpl
|
||||
// So offsetToXY (used by hover()) produces valid screen coordinates in a headless test.
|
||||
editor.component.setSize(800, 600)
|
||||
}
|
||||
|
||||
fun textOf(highlighter: RangeHighlighter): String {
|
||||
@@ -77,16 +124,39 @@ internal class TerminalOsc8HyperlinksEndToEndTest : BasePlatformTestCase() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the output editor's markup model until exactly one hyperlink highlighter is present.
|
||||
* Reconciliation between the output model and the markup model runs on its own delay
|
||||
* (see `installOsc8HyperlinksProcessing`), not synchronously with every output model change.
|
||||
* The target URI of the OSC8 link rendered as [highlighter], read from the output model - the
|
||||
* markup model's own decoration doesn't expose it (it's only used internally to build the click action).
|
||||
*/
|
||||
suspend fun awaitHyperlinkHighlighter(): RangeHighlighter {
|
||||
fun uriOf(highlighter: RangeHighlighter): String {
|
||||
val model = editor.getUserData(TerminalOutputModel.KEY)!!
|
||||
return model.getOsc8Hyperlinks().single {
|
||||
(it.startOffset - model.startOffset).toInt() == highlighter.startOffset &&
|
||||
(it.endOffset - model.startOffset).toInt() == highlighter.endOffset
|
||||
}.uri
|
||||
}
|
||||
|
||||
/** Moves the mouse over the middle of [highlighter]'s range, as a real mouse move would. */
|
||||
fun hover(highlighter: RangeHighlighter) {
|
||||
val offset = (highlighter.startOffset + highlighter.endOffset) / 2
|
||||
val point = editor.offsetToXY(offset)
|
||||
val event = MouseEvent(
|
||||
editor.contentComponent, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, point.x, point.y, 1, false, MouseEvent.BUTTON1
|
||||
)
|
||||
editor.contentComponent.mouseMotionListeners.forEach { it.mouseMoved(event) }
|
||||
}
|
||||
|
||||
suspend fun awaitHyperlink(): RangeHighlighter = awaitHyperlinks(1).single()
|
||||
|
||||
/**
|
||||
* Polls the output editor's markup model until exactly [count] hyperlink highlighters are present,
|
||||
* then returns them sorted by position.
|
||||
*/
|
||||
suspend fun awaitHyperlinks(count: Int): List<RangeHighlighter> {
|
||||
while (true) {
|
||||
val highlighters = withContext(Dispatchers.EDT) {
|
||||
editor.markupModel.allHighlighters.filter { it.isValid && it.layer == HighlighterLayer.HYPERLINK }
|
||||
}
|
||||
if (highlighters.size == 1) return highlighters.single()
|
||||
if (highlighters.size == count) return highlighters.sortedBy { it.startOffset }
|
||||
delay(50.milliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user