feat: Clickable links in outout (#900)
This commit is contained in:
parent
aa2bd95ccb
commit
7051aad599
|
|
@ -1,5 +1,6 @@
|
|||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import { Mutex } from './Mutex.js'
|
||||
|
||||
/**
|
||||
|
|
@ -18,13 +19,24 @@ export class OutputTerminal {
|
|||
constructor (executionTrackingId) {
|
||||
this.executionTrackingId = executionTrackingId
|
||||
this.writeMutex = new Mutex()
|
||||
const linkHandler = {
|
||||
activate (event, text, _range) {
|
||||
event.preventDefault()
|
||||
window.open(text, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
this.terminal = new Terminal({
|
||||
convertEol: true
|
||||
convertEol: true,
|
||||
linkHandler
|
||||
})
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
this.terminal.loadAddon(fitAddon)
|
||||
this.terminal.fit = fitAddon
|
||||
|
||||
this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
|
||||
this.linkHandlerConfigured = true
|
||||
}
|
||||
|
||||
async write (out, then) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@hugeicons/vue": "^1.0.4",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.14.0",
|
||||
|
|
@ -1570,6 +1571,12 @@
|
|||
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-web-links": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
|
||||
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -28,13 +28,14 @@
|
|||
"@hugeicons/vue": "^1.0.4",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.14.0",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.3"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
#
|
||||
# Integration Test Config: xterm link handling
|
||||
#
|
||||
|
||||
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||
|
||||
logLevel: "DEBUG"
|
||||
checkForUpdates: false
|
||||
|
||||
actions:
|
||||
- title: Echo URL
|
||||
shell: echo "See https://example.com for more info"
|
||||
popupOnStart: execution-dialog-stdout-only
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, it, before, after } from 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { By, Condition } from 'selenium-webdriver'
|
||||
import {
|
||||
getRootAndWait,
|
||||
takeScreenshotOnFailure,
|
||||
getTerminalBuffer,
|
||||
} from '../../lib/elements.js'
|
||||
|
||||
describe('config: xtermLinkHandling', function () {
|
||||
before(async function () {
|
||||
await runner.start('xtermLinkHandling')
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await runner.stop()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||
})
|
||||
|
||||
it('xterm output shows URL and link handling is configured', async function () {
|
||||
await getRootAndWait()
|
||||
|
||||
await webdriver.wait(new Condition('wait for Echo URL button', async () => {
|
||||
const btns = await webdriver.findElements(By.css('[title="Echo URL"]'))
|
||||
return btns.length === 1
|
||||
}), 10000)
|
||||
|
||||
const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]'))
|
||||
await echoUrlButton.click()
|
||||
|
||||
await webdriver.wait(new Condition('wait for execution view', async () => {
|
||||
const url = await webdriver.getCurrentUrl()
|
||||
return url.includes('/logs/') && !url.endsWith('/logs')
|
||||
}), 10000)
|
||||
|
||||
await webdriver.wait(new Condition('wait for execution status', async () => {
|
||||
const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
|
||||
return statusElements.length > 0
|
||||
}), 5000)
|
||||
|
||||
await webdriver.wait(new Condition('wait for execution to finish', async () => {
|
||||
try {
|
||||
const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
|
||||
const statusText = await statusElement.getText()
|
||||
return !statusText.includes('Executing')
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}), 5000)
|
||||
|
||||
await webdriver.sleep(500)
|
||||
|
||||
const bufferText = await getTerminalBuffer()
|
||||
expect(bufferText).to.not.be.null
|
||||
expect(bufferText).to.include('https://example.com')
|
||||
|
||||
const linkHandlerSet = await webdriver.executeScript(`
|
||||
try {
|
||||
return !!(window.terminal && window.terminal.linkHandlerConfigured === true)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
`)
|
||||
expect(linkHandlerSet).to.equal(true)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue