From 42e60434bdfc70f1a620392b33966e1994eb9f7d Mon Sep 17 00:00:00 2001 From: Shotaro-Kawaguchi Date: Thu, 16 Jan 2025 16:00:37 +0200 Subject: [PATCH 1/4] consoles: Add VNC dialogs --- src/components/vm/consoles/vncAdd.jsx | 122 ++++++++++++++++++++++++ src/components/vm/consoles/vncBody.jsx | 63 ++++++++++++ src/components/vm/consoles/vncEdit.jsx | 127 +++++++++++++++++++++++++ src/libvirtApi/domain.js | 26 +++++ 4 files changed, 338 insertions(+) create mode 100644 src/components/vm/consoles/vncAdd.jsx create mode 100644 src/components/vm/consoles/vncBody.jsx create mode 100644 src/components/vm/consoles/vncEdit.jsx diff --git a/src/components/vm/consoles/vncAdd.jsx b/src/components/vm/consoles/vncAdd.jsx new file mode 100644 index 000000000..457974c92 --- /dev/null +++ b/src/components/vm/consoles/vncAdd.jsx @@ -0,0 +1,122 @@ +/* + * This file is part of Cockpit. + * + * Copyright 2024 Fsas Technologies Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ +import React from 'react'; +import cockpit from 'cockpit'; +import PropTypes from 'prop-types'; +import { Button, Form, Modal, ModalVariant } from "@patternfly/react-core"; +import { DialogsContext } from 'dialogs.jsx'; + +import { ModalError } from 'cockpit-components-inline-notification.jsx'; +import { VncRow } from './vncBody.jsx'; +import { domainAttachVnc, domainGet } from '../../../libvirtApi/domain.js'; + +const _ = cockpit.gettext; + +export class AddVNC extends React.Component { + static contextType = DialogsContext; + + constructor(props) { + super(props); + + this.state = { + dialogError: undefined, + vncAddress: "", + vncPort: "", + vncPassword: "", + addVncInProgress: false, + }; + this.add = this.add.bind(this); + this.onValueChanged = this.onValueChanged.bind(this); + this.dialogErrorSet = this.dialogErrorSet.bind(this); + } + + onValueChanged(key, value) { + const stateDelta = { [key]: value }; + + this.setState(stateDelta); + } + + dialogErrorSet(text, detail) { + this.setState({ dialogError: text, dialogErrorDetail: detail }); + } + + add() { + const Dialogs = this.context; + const { vm } = this.props; + + this.setState({ addVncInProgress: true }); + const vncParams = { + connectionName: vm.connectionName, + vmName: vm.name, + vncAddress: this.state.vncAddress || "", + vncPort: this.state.vncPort || "", + vncPassword: this.state.vncPassword || "", + }; + + domainAttachVnc(vncParams) + .then(() => { + domainGet({ connectionName: vm.connectionName, id: vm.id }); + Dialogs.close(); + }) + .catch(exc => this.dialogErrorSet(_("VNC device settings could not be saved"), exc.message)) + .finally(() => this.setState({ addVncInProgress: false })); + } + + render() { + const Dialogs = this.context; + const { idPrefix } = this.props; + + const defaultBody = ( +
e.preventDefault()} isHorizontal> + + + ); + + return ( + + + + + }> + {this.state.dialogError && } + {defaultBody} + + ); + } +} + +AddVNC.propTypes = { + idPrefix: PropTypes.string.isRequired, + vm: PropTypes.object.isRequired, +}; + +export default AddVNC; diff --git a/src/components/vm/consoles/vncBody.jsx b/src/components/vm/consoles/vncBody.jsx new file mode 100644 index 000000000..f039f35fd --- /dev/null +++ b/src/components/vm/consoles/vncBody.jsx @@ -0,0 +1,63 @@ +/* + * This file is part of Cockpit. + * + * Copyright 2024 Fsas Technologies Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormGroup, Grid, GridItem, TextInput } from "@patternfly/react-core"; + +import cockpit from 'cockpit'; + +const _ = cockpit.gettext; + +export const VncRow = ({ idPrefix, onValueChanged, dialogValues }) => { + return ( + + + + onValueChanged('vncAddress', event.target.value)} /> + + + + + onValueChanged('vncPort', event.target.value)} /> + + + + + onValueChanged('vncPassword', event.target.value)} /> + + + + ); +}; + +VncRow.propTypes = { + idPrefix: PropTypes.string.isRequired, + onValueChanged: PropTypes.func.isRequired, + dialogValues: PropTypes.object.isRequired, +}; diff --git a/src/components/vm/consoles/vncEdit.jsx b/src/components/vm/consoles/vncEdit.jsx new file mode 100644 index 000000000..42f40d694 --- /dev/null +++ b/src/components/vm/consoles/vncEdit.jsx @@ -0,0 +1,127 @@ +/* + * This file is part of Cockpit. + * + * Copyright 2024 Fsas Technologies Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ +import React from 'react'; +import cockpit from 'cockpit'; +import PropTypes from 'prop-types'; +import { Button, Form, Modal, ModalVariant } from "@patternfly/react-core"; + +import { ModalError } from 'cockpit-components-inline-notification.jsx'; +import { DialogsContext } from 'dialogs.jsx'; +import { VncRow } from './vncBody.jsx'; +import { domainChangeVncSettings, domainGet } from '../../../libvirtApi/domain.js'; + +const _ = cockpit.gettext; + +export class EditVNCModal extends React.Component { + static contextType = DialogsContext; + + constructor(props) { + super(props); + + this.state = { + dialogError: undefined, + saveDisabled: false, + vmName: props.vmName, + vmId: props.vmId, + connectionName: props.connectionName, + vncAddress: props.consoleDetail.address || "", + vncPort: props.consoleDetail.port || "", + vncPassword: props.consoleDetail.password || "", + }; + + this.save = this.save.bind(this); + this.onValueChanged = this.onValueChanged.bind(this); + this.dialogErrorSet = this.dialogErrorSet.bind(this); + } + + onValueChanged(key, value) { + const stateDelta = { [key]: value }; + this.setState(stateDelta); + } + + dialogErrorSet(text, detail) { + this.setState({ dialogError: text, dialogErrorDetail: detail }); + } + + save() { + const Dialogs = this.context; + + const vncParams = { + connectionName: this.state.connectionName, + vmName: this.state.vmName, + vncAddress: this.state.vncAddress || "", + vncPort: this.state.vncPort || "", + vncPassword: this.state.vncPassword || "", + }; + + domainChangeVncSettings(vncParams) + .then(() => { + domainGet({ connectionName: this.state.connectionName, id: this.state.vmId }); + Dialogs.close(); + }) + .catch((exc) => { + this.dialogErrorSet(_("VNC settings could not be saved"), exc.message); + }); + } + + render() { + const Dialogs = this.context; + const { idPrefix } = this.props; + + const defaultBody = ( +
e.preventDefault()} isHorizontal> + + + ); + const showWarning = () => { + }; + + return ( + + + + + }> + <> + { showWarning() } + {this.state.dialogError && } + {defaultBody} + + + ); + } +} +EditVNCModal.propTypes = { + idPrefix: PropTypes.string.isRequired, + vmName: PropTypes.string.isRequired, + vmId: PropTypes.string.isRequired, + connectionName: PropTypes.string.isRequired, + consoleDetail: PropTypes.object.isRequired, +}; + +export default EditVNCModal; diff --git a/src/libvirtApi/domain.js b/src/libvirtApi/domain.js index eab234179..b7f8ea42e 100644 --- a/src/libvirtApi/domain.js +++ b/src/libvirtApi/domain.js @@ -1089,3 +1089,29 @@ export async function domainAddTPM({ connectionName, vmName }) { const args = ["virt-xml", "-c", `qemu:///${connectionName}`, "--add-device", "--tpm", "default", vmName]; return cockpit.spawn(args, { err: "message", superuser: connectionName === "system" ? "try" : null }); } + +export function domainAttachVnc({ connectionName, vmName, vncAddress, vncPort, vncPassword }) { + const args = ['virt-xml', '-c', `qemu:///${connectionName}`, vmName, '--add-device', '--graphics', `vnc,listen=${vncAddress},port=${vncPort},passwd=${vncPassword}`]; + const options = { err: "message" }; + + if (connectionName === "system") + options.superuser = "try"; + + return cockpit.spawn(args, options); +} + +export function domainChangeVncSettings({ + connectionName, + vmName, + vncAddress, + vncPort, + vncPassword, +}) { + const options = { err: "message" }; + if (connectionName === "system") + options.superuser = "try"; + + const args = ["virt-xml", "-c", `qemu:///${connectionName}`, vmName, "--edit", "--graphics", `vnc,listen=${vncAddress},port=${vncPort},passwd=${vncPassword}`]; + + return cockpit.spawn(args, options); +} From c66e290c0264b9d495a995cde39c5160b863823b Mon Sep 17 00:00:00 2001 From: Marius Vollmer Date: Wed, 29 Jan 2025 10:57:15 +0200 Subject: [PATCH 2/4] consoles: Improve VNC dialogs - The dialogs talk about "server" and "listening" to make that clearer. - We use the empty string instead of "-1" to signify automatic port assignment in the UI. - The port does validation of its value. - The password field takes the whole row and has can reveal its value. - The dialogs warn if a shutdown is needed. - The components take the whole "vm" object instead of separate name, id, and connectionName. --- src/components/vm/consoles/vncAdd.jsx | 27 +++++--- src/components/vm/consoles/vncBody.jsx | 94 ++++++++++++++++++-------- src/components/vm/consoles/vncEdit.jsx | 44 ++++++------ 3 files changed, 108 insertions(+), 57 deletions(-) diff --git a/src/components/vm/consoles/vncAdd.jsx b/src/components/vm/consoles/vncAdd.jsx index 457974c92..befe19026 100644 --- a/src/components/vm/consoles/vncAdd.jsx +++ b/src/components/vm/consoles/vncAdd.jsx @@ -23,9 +23,11 @@ import { Button, Form, Modal, ModalVariant } from "@patternfly/react-core"; import { DialogsContext } from 'dialogs.jsx'; import { ModalError } from 'cockpit-components-inline-notification.jsx'; -import { VncRow } from './vncBody.jsx'; +import { VncRow, validateDialogValues } from './vncBody.jsx'; import { domainAttachVnc, domainGet } from '../../../libvirtApi/domain.js'; +import { NeedsShutdownAlert } from '../../common/needsShutdown.jsx'; + const _ = cockpit.gettext; export class AddVNC extends React.Component { @@ -40,6 +42,7 @@ export class AddVNC extends React.Component { vncPort: "", vncPassword: "", addVncInProgress: false, + validationErrors: { }, }; this.add = this.add.bind(this); this.onValueChanged = this.onValueChanged.bind(this); @@ -47,8 +50,7 @@ export class AddVNC extends React.Component { } onValueChanged(key, value) { - const stateDelta = { [key]: value }; - + const stateDelta = { [key]: value, validationErrors: { } }; this.setState(stateDelta); } @@ -60,6 +62,12 @@ export class AddVNC extends React.Component { const Dialogs = this.context; const { vm } = this.props; + const errors = validateDialogValues(this.state); + if (errors) { + this.setState({ validationErrors: errors }); + return; + } + this.setState({ addVncInProgress: true }); const vncParams = { connectionName: vm.connectionName, @@ -80,19 +88,21 @@ export class AddVNC extends React.Component { render() { const Dialogs = this.context; - const { idPrefix } = this.props; + const { idPrefix, vm } = this.props; const defaultBody = (
e.preventDefault()} isHorizontal> - + ); return ( + + + ); }; +export function validateDialogValues(values) { + const res = { }; + + if (values.vncPort == "") + ; // fine + else if (!values.vncPort.match("^[0-9]+$") || Number(values.vncPort) < 5900) + res.vncPort = _("Port must be 5900 or larger."); + + return Object.keys(res).length > 0 ? res : null; +} + VncRow.propTypes = { idPrefix: PropTypes.string.isRequired, onValueChanged: PropTypes.func.isRequired, dialogValues: PropTypes.object.isRequired, + validationErrors: PropTypes.object.isRequired, }; diff --git a/src/components/vm/consoles/vncEdit.jsx b/src/components/vm/consoles/vncEdit.jsx index 42f40d694..5dcb034ac 100644 --- a/src/components/vm/consoles/vncEdit.jsx +++ b/src/components/vm/consoles/vncEdit.jsx @@ -23,8 +23,9 @@ import { Button, Form, Modal, ModalVariant } from "@patternfly/react-core"; import { ModalError } from 'cockpit-components-inline-notification.jsx'; import { DialogsContext } from 'dialogs.jsx'; -import { VncRow } from './vncBody.jsx'; +import { VncRow, validateDialogValues } from './vncBody.jsx'; import { domainChangeVncSettings, domainGet } from '../../../libvirtApi/domain.js'; +import { NeedsShutdownAlert } from '../../common/needsShutdown.jsx'; const _ = cockpit.gettext; @@ -37,12 +38,10 @@ export class EditVNCModal extends React.Component { this.state = { dialogError: undefined, saveDisabled: false, - vmName: props.vmName, - vmId: props.vmId, - connectionName: props.connectionName, vncAddress: props.consoleDetail.address || "", - vncPort: props.consoleDetail.port || "", + vncPort: Number(props.consoleDetail.port) == -1 ? "" : props.consoleDetail.port || "", vncPassword: props.consoleDetail.password || "", + validationErrors: { }, }; this.save = this.save.bind(this); @@ -51,7 +50,7 @@ export class EditVNCModal extends React.Component { } onValueChanged(key, value) { - const stateDelta = { [key]: value }; + const stateDelta = { [key]: value, validationErrors: { } }; this.setState(stateDelta); } @@ -61,10 +60,17 @@ export class EditVNCModal extends React.Component { save() { const Dialogs = this.context; + const { vm } = this.props; + + const errors = validateDialogValues(this.state); + if (errors) { + this.setState({ validationErrors: errors }); + return; + } const vncParams = { - connectionName: this.state.connectionName, - vmName: this.state.vmName, + connectionName: vm.connectionName, + vmName: vm.name, vncAddress: this.state.vncAddress || "", vncPort: this.state.vncPort || "", vncPassword: this.state.vncPassword || "", @@ -72,7 +78,7 @@ export class EditVNCModal extends React.Component { domainChangeVncSettings(vncParams) .then(() => { - domainGet({ connectionName: this.state.connectionName, id: this.state.vmId }); + domainGet({ connectionName: vm.connectionName, id: vm.id }); Dialogs.close(); }) .catch((exc) => { @@ -82,21 +88,21 @@ export class EditVNCModal extends React.Component { render() { const Dialogs = this.context; - const { idPrefix } = this.props; + const { idPrefix, vm } = this.props; const defaultBody = (
e.preventDefault()} isHorizontal> - + ); - const showWarning = () => { - }; return ( + + + ); + } + + let vnc_info; + let vnc_action; + + if (!vnc) { + vnc_info = _("not supported"); + vnc_action = ( + + ); + } else { + if (vnc.port == -1) + vnc_info = _("VNC, dynamic port"); + else + vnc_info = cockpit.format(_("VNC, port $0"), vnc.port); + + vnc_action = ( + + ); + } + + return ( + <> +

+ { + vm.state == "running" + ? _("Shut down and restart the virtual machine to access the graphical console.") + : _("Please start the virtual machine to access its console.") + } +

+
+
+ + + {_("Graphical console:")} {vnc_info} + + + {vnc_action} + + +
+ + ); +}; + class Vnc extends React.Component { constructor(props) { super(props); @@ -115,9 +195,18 @@ class Vnc extends React.Component { } render() { - const { consoleDetail, connectionName, vmName, vmId, onAddErrorNotification, isExpanded } = this.props; + const { consoleDetail, inactiveConsoleDetail, vm, onAddErrorNotification, isExpanded } = this.props; const { path, isActionOpen } = this.state; - if (!consoleDetail || !path) { + + if (!consoleDetail) { + return ( +
+ +
+ ); + } + + if (!path) { // postpone rendering until consoleDetail is known and channel ready return null; } @@ -129,11 +218,11 @@ class Vnc extends React.Component { id={cockpit.format("ctrl-alt-$0", keyName)} key={cockpit.format("ctrl-alt-$0", keyName)} onClick={() => { - return domainSendKey({ connectionName, id: vmId, keyCodes: [Enum.KEY_LEFTCTRL, Enum.KEY_LEFTALT, Enum[cockpit.format("KEY_$0", keyName.toUpperCase())]] }) + return domainSendKey({ connectionName: vm.connectionName, id: vm.id, keyCodes: [Enum.KEY_LEFTCTRL, Enum.KEY_LEFTALT, Enum[cockpit.format("KEY_$0", keyName.toUpperCase())]] }) .catch(ex => onAddErrorNotification({ - text: cockpit.format(_("Failed to send key Ctrl+Alt+$0 to VM $1"), keyName, vmName), + text: cockpit.format(_("Failed to send key Ctrl+Alt+$0 to VM $1"), keyName, vm.name), detail: ex.message, - resourceId: vmId, + resourceId: vm.id, })); }}> {cockpit.format(_("Ctrl+Alt+$0"), keyName)} @@ -147,10 +236,10 @@ class Vnc extends React.Component { ]; const additionalButtons = [ ( this.setState({ isActionOpen: !isActionOpen })}> {_("Send key")} diff --git a/src/components/vm/consoles/vncBody.jsx b/src/components/vm/consoles/vncBody.jsx index 33731ffe4..1c9494189 100644 --- a/src/components/vm/consoles/vncBody.jsx +++ b/src/components/vm/consoles/vncBody.jsx @@ -21,7 +21,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { FormGroup, FormHelperText, HelperText, HelperTextItem, - InputGroup, TextInput, Button, Checkbox, + InputGroup, TextInput, Button, Checkbox } from "@patternfly/react-core"; import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons"; diff --git a/src/components/vm/vmDetailsPage.jsx b/src/components/vm/vmDetailsPage.jsx index 98c38a335..a8e36465a 100644 --- a/src/components/vm/vmDetailsPage.jsx +++ b/src/components/vm/vmDetailsPage.jsx @@ -132,24 +132,22 @@ export const VmDetailsPage = ({ title: _("Usage"), body: , }, - ...(vm.displays.length - ? [{ - id: `${vmId(vm.name)}-consoles`, - className: "consoles-card", - title: _("Console"), - actions: vm.state != "shut off" - ? - : null, - body: , - }] - : []), + { + id: `${vmId(vm.name)}-consoles`, + className: "consoles-card", + title: _("Console"), + actions: vm.state != "shut off" + ? + : null, + body: , + }, { id: `${vmId(vm.name)}-disks`, className: "disks-card", diff --git a/test/check-machines-consoles b/test/check-machines-consoles index 3b8daf778..17b848515 100755 --- a/test/check-machines-consoles +++ b/test/check-machines-consoles @@ -19,6 +19,7 @@ import os import time +import xml.etree.ElementTree as ET import machineslib import testlib @@ -324,6 +325,104 @@ fullscreen=0 self.waitViewerDownload("vnc", my_ip) + def testAddEditVNC(self): + b = self.browser + + # Create a machine without any consoles + + name = "subVmTest1" + self.createVm(name) + + self.login_and_go("/machines") + self.waitPageInit() + self.waitVmRow(name) + self.goToVmPage(name) + + # "Console" card shows empty state + + b.wait_in_text(f"#vm-{name}-consoles .pf-v5-c-empty-state", "Graphical support not enabled.") + b.assert_pixels(f"#vm-{name}-consoles", "no-vnc") + + b.click(f"#vm-{name}-consoles .pf-v5-c-empty-state button:contains(Add VNC)") + + b.wait_visible("#add-vnc-dialog") + b.set_checked("#add-vnc-autoport", val=True) + b.set_input_text("#add-vnc-port", "5000") + b.click("#add-vnc-add") + b.wait_visible("#add-vnc-dialog .pf-m-error:contains('Port must be 5900 or larger.')") + b.assert_pixels("#add-vnc-dialog", "add") + b.set_input_text("#add-vnc-port", "100000000000") # for testing failed libvirt calls + b.set_input_text("#add-vnc-password", "foobar") + b.wait_attr("#add-vnc-password", "type", "password") + b.click("#add-vnc-dialog .pf-v5-c-input-group button") + b.wait_attr("#add-vnc-password", "type", "text") + b.click("#add-vnc-add") + b.wait_in_text("#add-vnc-dialog", "VNC device settings could not be saved") + b.wait_in_text("#add-vnc-dialog", "cannot parse vnc port 100000000000") + b.set_input_text("#add-vnc-port", "5901") + b.click("#add-vnc-add") + b.wait_not_present("#add-vnc-dialog") + + b.wait_in_text(f"#vm-{name}-consoles .pf-v5-c-console", "Shut down and restart") + b.wait_in_text(f"#vm-{name}-consoles .pf-v5-c-console", "Graphical console: VNC, port 5901") + b.wait_visible(f"#vm-{name}-needs-shutdown") + b.assert_pixels(f"#vm-{name}-consoles", "needs-shutdown") + + root = ET.fromstring(self.machine.execute(f"virsh dumpxml --inactive --security-info {name}")) + graphics = root.find('devices').findall('graphics') + self.assertEqual(len(graphics), 1) + self.assertEqual(graphics[0].get('port'), "5901") + self.assertEqual(graphics[0].get('passwd'), "foobar") + + b.click(f"#vm-{name}-consoles .pf-v5-c-console button:contains(Edit)") + b.wait_visible("#edit-vnc-dialog") + b.wait_val("#edit-vnc-port", "5901") + b.wait_val("#edit-vnc-password", "foobar") + b.assert_pixels("#edit-vnc-dialog", "edit") + b.set_input_text("#edit-vnc-port", "100000000000") # for testing failed libvirt calls + b.click("#edit-vnc-save") + b.wait_in_text("#edit-vnc-dialog", "VNC settings could not be saved") + b.wait_in_text("#edit-vnc-dialog", "cannot parse vnc port 100000000000") + b.set_checked("#edit-vnc-autoport", val=False) + b.click("#edit-vnc-save") + b.wait_not_present("#edit-vnc-dialog") + + b.wait_in_text(f"#vm-{name}-consoles .pf-v5-c-console", "Graphical console: VNC, dynamic port") + + root = ET.fromstring(self.machine.execute(f"virsh dumpxml --inactive --security-info {name}")) + graphics = root.find('devices').findall('graphics') + self.assertEqual(len(graphics), 1) + self.assertEqual(graphics[0].get('port'), "-1") + self.assertEqual(graphics[0].get('passwd'), "foobar") + + # Shut down machine + + self.performAction("subVmTest1", "forceOff") + b.wait_in_text("#vm-not-running-message", "Graphical console: VNC, dynamic port") + b.assert_pixels(f"#vm-{name}-consoles", "shutoff") + + # Remove VNC from the outside and do the whole dance again + + self.machine.execute(f"virt-xml --remove-device --graphics vnc {name}") + b.wait_in_text("#vm-not-running-message", "Graphical console: not supported") + + b.click("#vm-not-running-message button:contains(Add VNC)") + b.wait_visible("#add-vnc-dialog") + b.click("#add-vnc-add") + b.wait_not_present("#add-vnc-dialog") + b.wait_in_text("#vm-not-running-message", "Graphical console: VNC, dynamic port") + + b.click("#vm-not-running-message button:contains(Edit)") + b.wait_visible("#edit-vnc-dialog") + b.set_checked("#edit-vnc-autoport", val=True) + b.set_input_text("#edit-vnc-port", "5000") + b.click("#edit-vnc-save") + b.wait_visible("#edit-vnc-dialog .pf-m-error:contains('Port must be 5900 or larger.')") + b.set_input_text("#edit-vnc-port", "5900") + b.click("#edit-vnc-save") + b.wait_not_present("#edit-vnc-dialog") + b.wait_in_text("#vm-not-running-message", "Graphical console: VNC, port 5900") + if __name__ == '__main__': testlib.test_main() diff --git a/test/reference b/test/reference index fef884db2..405dc8e13 160000 --- a/test/reference +++ b/test/reference @@ -1 +1 @@ -Subproject commit fef884db2f402038be5b2f37e0df9d45a7aca4c2 +Subproject commit 405dc8e13d990ab893659e4ed12285c40f3304a1