mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
When the server refused a VM create submission (a field error, a busy resource, a reused submission key, an existing VM name, a missing MAC for a new disk), the page came back on its first tab with the flavor dialog closed and every entered value gone: the hand-written fields showed fixed defaults, and the errors sat in a block at the top of the page, outside #messages and tied to no field. The view builds one NewVMForm per create form on the page (custom, template, one per flavor; create_forms), each with its own defaults; a hidden form_id, which is also the prefix of the form's field ids, names the submitted one, and it comes back bound. Every field renders from its own form and is marked by two small template tags (form_fields: error_attrs, field_error) when refused; a summary at the top of the form lists all of its errors, also those of fields it keeps hidden. The submitted tab or flavor dialog, marked data-form-id, reopens with any "Advanced" section that holds an error; a reopened dialog shows the messages too; the template form gets its pool, template and storage back. base.html's error focus is a function that walks the places an error can be, and a page can call it once it is set up, as the create page does. form_id changes nothing in how a POST is processed; one of an unknown form (a flavor deleted since) has its errors in the messages. The default video now applies to every create form. The custom form's disks and networks are not restored yet.
89 lines
5.1 KiB
Python
89 lines
5.1 KiB
Python
"""Scripts of the VM create page, rendered on libvirt's test driver and run
|
|
in Node against stand-ins."""
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from instances.forms import NewVMForm
|
|
from instances.tests.create_page import render_create_page
|
|
from lxml import html
|
|
|
|
|
|
class CreatePageScriptTests(TestCase):
|
|
def page(self):
|
|
compute, response = render_create_page(self)
|
|
self.assertEqual(response.status_code, 200)
|
|
return compute, response.content.decode()
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
|
def test_the_disk_bus_list_is_asked_for_the_page_architecture_and_machine(self):
|
|
compute, page = self.page()
|
|
function = re.search(r"function get_disk_bus_choices\(.*?\n }\n", page, re.S).group(0)
|
|
script = "let asked; const $ = Object.assign(() => ({find: () => ({remove() {}}), append() {}, val() {}}), " \
|
|
"{getJSON: url => { asked = url; }, each() {}});\n" + function + \
|
|
f"get_disk_bus_choices({compute.id}, 0, 'cdrom'); console.log(asked);"
|
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(result.stdout.strip(), reverse("buses", args=[compute.id, "i686", "pc", "cdrom"]))
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
|
def test_the_custom_form_sends_its_own_macs_and_leaves_the_others_alone(self):
|
|
compute, page = self.page()
|
|
custom = html.fromstring(page).xpath("//form[@aria-label='Create custom instance form']")[0]
|
|
self.assertEqual(custom.xpath(".//input[@name='mac']/@id"), ["custom_mac"])
|
|
function = re.search(r"function populate_mac\(.*?\n }\n", page, re.S).group(0)
|
|
# the stand-in $ records each write; a MAC field holds what its selector names
|
|
script = "const writes = []; let mac_array = [];\n" \
|
|
"const $ = sel => ({val: v => v === undefined ? sel.replace(/\\D/g, '') : writes.push([sel, v])});\n" + \
|
|
function + "populate_mac(0); populate_mac(1); console.log(JSON.stringify(writes));"
|
|
result = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(json.loads(result.stdout), [["#custom_mac", "0"], ["#custom_mac", "0,1"]])
|
|
|
|
def test_a_mac_per_nic_fits_the_form(self):
|
|
macs = ",".join(f"52:54:00:00:00:{n:02x}" for n in range(8))
|
|
self.assertEqual(NewVMForm.base_fields["mac"].clean(macs), macs)
|
|
|
|
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
|
def test_the_page_reopens_the_submitted_form(self):
|
|
_, page = self.page()
|
|
functions = re.search(r"function reopen_submitted_form\(.*?\n \$\(reopen_submitted_form\);", page, re.S).group(0)
|
|
harness = """
|
|
const shown = [];
|
|
// the forms' holders: two tab panes and a flavor's dialog, each with an
|
|
// "Advanced" section that holds a refused field; a dialog's "shown"
|
|
// listeners run when it is shown, as in Bootstrap
|
|
const holder = (id, kind) => ({id, listeners: [], classList: {contains: c => c === kind},
|
|
querySelectorAll: () => [{name: "#" + id + " .collapse", querySelector: () => ({})}],
|
|
addEventListener(event, f) { this.listeners.push(f); }});
|
|
const holders = {custom: holder("addCustom", "tab-pane"), template: holder("add-from-temp", "tab-pane"),
|
|
flavor7: holder("addVMflavor7", "modal")};
|
|
const document = {body: {dataset: {submittedForm: %s}}, querySelector: sel => {
|
|
const id = (sel.match(/data-form-id="([^"]*)"/) || [])[1];
|
|
if (id !== undefined) return holders[id] || null;
|
|
return sel.startsWith("form[") ? null : {name: sel}; // no refused template form here
|
|
}};
|
|
const bootstrap = {Tab: {getOrCreateInstance: e => ({show: () => shown.push("tab " + e.name)})},
|
|
Modal: {getOrCreateInstance: e => ({show: () => { shown.push("modal #" + e.id); e.listeners.forEach(f => f()); }})},
|
|
Collapse: {getOrCreateInstance: e => ({show: () => shown.push("collapse " + e.name)})}};
|
|
const $ = Object.assign(() => ({val() {}}), {when: () => ({then: f => f()})});
|
|
const focusFirstError = () => shown.push("focus");
|
|
%s
|
|
reopen_submitted_form(); console.log(JSON.stringify(shown));
|
|
"""
|
|
for submitted, expected in (
|
|
("custom", ["collapse #addCustom .collapse", 'tab [data-bs-target="#addCustom"]', "focus"]),
|
|
("template", ["collapse #add-from-temp .collapse", 'tab [data-bs-target="#add-from-temp"]', "focus"]),
|
|
("flavor7", ["collapse #addVMflavor7 .collapse", "modal #addVMflavor7", "focus"]),
|
|
("", ["focus"]),
|
|
):
|
|
with self.subTest(submitted=submitted):
|
|
result = subprocess.run(["node", "-e", harness % (json.dumps(submitted), functions.replace("$(reopen_submitted_form);", ""))],
|
|
capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(json.loads(result.stdout), expected)
|