diff --git a/CHANGES.md b/CHANGES.md
index 2510423d..8fb15d02 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,6 +27,7 @@
- Bump typescript from 5.4.3 to 5.6.2
- Bump webpack from 5.91.0 to 5.94.0
- Bump ws from 8.17.0 to 8.17.1
+- Reverted JENKINS-71365, pull request #79, making the Stapler proxy synchronous again, to ensure determinism in parameters resolution, fixing a regression
- Update pom.xml to switch from node 18.16 to 18.18 (for eslint 9)
- Update pom.xml to bump Jenkins version to Jenkins 2.462.2 (job-dsl requirement)
diff --git a/pom.xml b/pom.xml
index d8c490cc..d6f4173b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,7 +40,7 @@
jenkinsci/active-choices-plugin
18.18.0
1.22.19
- PT60S
+ PT300S
diff --git a/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/config.jelly b/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/config.jelly
index 56a60eed..b1b89347 100644
--- a/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/config.jelly
+++ b/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/config.jelly
@@ -48,13 +48,13 @@
-
+
-
+
${%Filterable}
-
+
diff --git a/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/index.jelly b/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/index.jelly
index 8c958bd9..280a5184 100644
--- a/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/index.jelly
+++ b/src/main/resources/org/biouno/unochoice/CascadeChoiceParameter/index.jelly
@@ -7,10 +7,18 @@
// source, references table
var referencedParameters = Array();
- // add the element we want to monitor
- referencedParameters.push("${value}");
+ // add the element we want to monitor
+ referencedParameters.push("${value}");
+ if (window.makeStaplerProxy) {
+ window.__old__makeStaplerProxy = window.makeStaplerProxy;
+ window.makeStaplerProxy = UnoChoice.makeStaplerProxy2;
+ }
+ var cascadeChoiceParameter = ; // Create Jenkins proxy
+ if (window.makeStaplerProxy) {
+ window.makeStaplerProxy = window.__old__makeStaplerProxy;
+ }
UnoChoice.renderCascadeChoiceParameter('#${h.escape(paramName)}', ${it.filterable}, '${h.escape(it.getName())}', '${h.escape(it.getRandomName())}', ${it.getFilterLength()}, '${h.escape(paramName)}', referencedParameters, cascadeChoiceParameter);
diff --git a/src/main/resources/org/biouno/unochoice/ChoiceParameter/config.jelly b/src/main/resources/org/biouno/unochoice/ChoiceParameter/config.jelly
index f0391b32..5c03cc32 100644
--- a/src/main/resources/org/biouno/unochoice/ChoiceParameter/config.jelly
+++ b/src/main/resources/org/biouno/unochoice/ChoiceParameter/config.jelly
@@ -48,10 +48,10 @@
-
+
${%Filterable}
-
+
diff --git a/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/config.jelly b/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/config.jelly
index c5edcfd5..d7ad4cba 100644
--- a/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/config.jelly
+++ b/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/config.jelly
@@ -56,11 +56,11 @@
-
+
-
+
${%Omit value field}
diff --git a/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/index.jelly b/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/index.jelly
index 1309375c..b363f24d 100644
--- a/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/index.jelly
+++ b/src/main/resources/org/biouno/unochoice/DynamicReferenceParameter/index.jelly
@@ -60,10 +60,18 @@
// source, references table
var referencedParameters = Array();
- // add the element we want to monitor
- referencedParameters.push("${value}");
+ // add the element we want to monitor
+ referencedParameters.push("${value}");
+ if (window.makeStaplerProxy) {
+ window.__old__makeStaplerProxy = window.makeStaplerProxy;
+ window.makeStaplerProxy = UnoChoice.makeStaplerProxy2;
+ }
+ var dynamicReferenceParameter = ; // Create Jenkins proxy
+ if (window.makeStaplerProxy) {
+ window.makeStaplerProxy = window.__old__makeStaplerProxy;
+ }
UnoChoice.renderDynamicRenderParameter('#${paramName}', '${h.escape(it.getName())}', '${h.escape(paramName)}', referencedParameters, dynamicReferenceParameter);
// update spinner id
diff --git a/src/main/resources/org/biouno/unochoice/common/radioContent.jelly b/src/main/resources/org/biouno/unochoice/common/radioContent.jelly
index f73f6d2c..66ea1770 100644
--- a/src/main/resources/org/biouno/unochoice/common/radioContent.jelly
+++ b/src/main/resources/org/biouno/unochoice/common/radioContent.jelly
@@ -113,7 +113,7 @@
maxCount = ${it.visibleItemCount};
}
- var refElement = document.getElementById("${id}");
+ var refElement = document.getElementById("ecp_${h.escape(it.randomName)}_0");
if(maxCount > 0 && refElement && refElement.offsetHeight !=0) {
for(var i=0; i< maxCount; i++) {
height += refElement.offsetHeight + 3;
diff --git a/src/main/resources/org/biouno/unochoice/model/ScriptlerScript/config.jelly b/src/main/resources/org/biouno/unochoice/model/ScriptlerScript/config.jelly
index ca39aaa1..b048bd6c 100644
--- a/src/main/resources/org/biouno/unochoice/model/ScriptlerScript/config.jelly
+++ b/src/main/resources/org/biouno/unochoice/model/ScriptlerScript/config.jelly
@@ -3,7 +3,7 @@
This module depends on JQuery only.
*
- * @param $ jQuery3 global var
+ * @param jQuery3 jQuery3 global var
* @author Bruno P. Kinoshita
* @since 0.20
*/
-var UnoChoice = UnoChoice || ($ => {
- let util = new Util($);
+var UnoChoice = UnoChoice || (jQuery3 => {
+ let util = new Util(jQuery3);
// The final public object
let instance = {};
let SEPARATOR = '__LESEP__';
@@ -148,18 +148,18 @@ var UnoChoice = UnoChoice || ($ => {
let parametersString = this.getReferencedParametersAsText(); // gets the array parameters, joined by , (e.g. a,b,c,d)
console.log(`Values retrieved from Referenced Parameters: ${parametersString}`);
// Update the CascadeChoiceParameter Map of parameters
- await new Promise((resolve) => this.proxy.doUpdate(parametersString, t => resolve(t)));
+ await this.proxy.doUpdate(parametersString);
let spinner, rootDiv;
if (this.getRandomName()) {
let spinnerId = this.getRandomName().split('_').pop();
- spinner = jQuery(`div#${spinnerId}-spinner`);
+ spinner = jQuery3(`div#${spinnerId}-spinner`);
// Show spinner
if (spinner) {
spinner.show();
}
// Disable DIV changes
- rootDiv = jQuery(`div#${spinnerId}`);
+ rootDiv = jQuery3(`div#${spinnerId}`);
if (rootDiv) {
rootDiv.css('pointer-events', 'none');
}
@@ -169,7 +169,7 @@ var UnoChoice = UnoChoice || ($ => {
// The inner function is called with the response provided by Stapler. Then we update the HTML elements.
let _self = this; // re-reference this to use within the inner function
console.log('Calling Java server code to update HTML elements...');
- await new Promise((resolve) => this.proxy.getChoicesForUI(t => {
+ await this.proxy.getChoicesForUI(t => {
let data = t.responseObject();
console.log(`Values returned from server: ${data}`);
let newValues = data[0];
@@ -240,7 +240,7 @@ var UnoChoice = UnoChoice || ($ => {
} else if (parameterElement.tagName === 'DIV' || parameterElement.tagName === 'SPAN') {
if (parameterElement.children.length > 0 && (parameterElement.children[0].tagName === 'DIV' || parameterElement.children[0].tagName === 'SPAN')) {
let tbody = parameterElement.children[0];
- $(tbody).empty();
+ jQuery3(tbody).empty();
let originalArray = [];
// Check whether it is a radio or checkbox element
if (parameterElement.className === 'dynamic_checkbox') {
@@ -311,12 +311,11 @@ var UnoChoice = UnoChoice || ($ => {
parameterElement.style.height = newValues.length > 10 ? '230px' : 'auto';
} // if (parameterElement.children.length > 0 && parameterElement.children[0].tagName === 'DIV') {
} // if (parameterElement.tagName === 'SELECT') { // } else if (parameterElement.tagName === 'DIV') {
- resolve(t)
- }));
+ });
// propagate change
// console.log('Propagating change event from ' + this.getParameterName());
// let e1 = $.Event('change', {parameterName: this.getParameterName()});
- // $(this.getParameterElement()).trigger(e1);
+ // jQuery3(this.getParameterElement()).trigger(e1);
if (!avoidRecursion) {
if (cascadeParameters && cascadeParameters.length > 0) {
for (let i = 0; i < cascadeParameters.length; i++) {
@@ -342,7 +341,7 @@ var UnoChoice = UnoChoice || ($ => {
/**
* Returns true
iff the given parameter is not null, and one of its
* reference parameters is the same parameter as this
. In other words,
- * it returns whether or not the given parameter references this parameter.
+ * it returns whether the given parameter references this parameter.
*
* @since 0.22
* @param cascadeParameter {CascadeParameter} a given parameter
@@ -377,18 +376,18 @@ var UnoChoice = UnoChoice || ($ => {
this.cascadeParameter = cascadeParameter;
// Add event listener
let _self = this;
- $(this.paramElement).change(e => {
+ jQuery3(this.paramElement).change(e => {
if (e.parameterName === _self.paramName) {
console.log('Skipping self reference to avoid infinite loop!');
e.stopImmediatePropagation();
} else {
console.log(`Cascading changes from parameter ${_self.paramName}...`);
//_self.cascadeParameter.loading(true);
- $(".behavior-loading").show();
+ jQuery3(".behavior-loading").show();
// start updating in separate async function so browser will be able to repaint and show 'loading' animation , see JENKINS-34487
setTimeout(async () => {
- await _self.cascadeParameter.update(false);
- $(".behavior-loading").hide();
+ await _self.cascadeParameter.update(false);
+ jQuery3(".behavior-loading").hide();
}, 0);
}
});
@@ -441,18 +440,18 @@ var UnoChoice = UnoChoice || ($ => {
let parametersString = this.getReferencedParametersAsText(); // gets the array parameters, joined by , (e.g. a,b,c,d)
console.log(`Values retrieved from Referenced Parameters: ${parametersString}`);
// Update the Map of parameters
- await new Promise((resolve) => this.proxy.doUpdate(parametersString, t => resolve(t)));
+ await this.proxy.doUpdate(parametersString);
let parameterElement = this.getParameterElement();
let spinner, rootDiv;
if (parameterElement.id) {
let spinnerId = parameterElement.id.split('_').pop();
- spinner = jQuery(`div#${spinnerId}-spinner`);
+ spinner = jQuery3(`div#${spinnerId}-spinner`);
// Show spinner
if (spinner) {
spinner.show();
}
- rootDiv = jQuery(`div#${spinnerId}`);
+ rootDiv = jQuery3(`div#${spinnerId}`);
// Disable DIV changes
if (rootDiv) {
rootDiv.css('pointer-events', 'none');
@@ -462,9 +461,9 @@ var UnoChoice = UnoChoice || ($ => {
// or maybe call a string to put as value in a INPUT.
if (parameterElement.tagName === 'OL') { // handle OL's
console.log('Calling Java server code to update HTML elements...');
- await new Promise((resolve) => this.proxy.getChoicesForUI(t => {
- $(parameterElement).empty(); // remove all children elements
- let data = t.responseObject();
+ await this.proxy.getChoicesForUI(t => {
+ jQuery3(parameterElement).empty(); // remove all children elements
+ const data = t.responseObject();
console.log(`Values returned from server: ${data}`);
let newValues = data[0];
// let newKeys = data[1];
@@ -473,13 +472,12 @@ var UnoChoice = UnoChoice || ($ => {
li.innerHTML = newValues[i];
parameterElement.appendChild(li); // append new elements
}
- resolve(t)
- }));
+ });
} else if (parameterElement.tagName === 'UL') { // handle OL's
- $(parameterElement).empty(); // remove all children elements
+ jQuery3(parameterElement).empty(); // remove all children elements
console.log('Calling Java server code to update HTML elements...');
- await new Promise(resolve => this.proxy.getChoicesForUI(t => {
- let data = t.responseObject();
+ await this.proxy.getChoicesForUI(t => {
+ const data = t.responseObject();
console.log(`Values returned from server: ${data}`);
let newValues = data[0];
// let newKeys = data[1];
@@ -488,23 +486,20 @@ var UnoChoice = UnoChoice || ($ => {
li.innerHTML = newValues[i];
parameterElement.appendChild(li); // append new elements
}
- resolve(t)
- }));
+ });
} else if (parameterElement.id.indexOf('inputElement_') > -1) { // handle input text boxes
- await new Promise(resolve => this.proxy.getChoicesAsStringForUI(t => {
- parameterElement.value = t.responseObject();
- resolve(t)
- }));
+ await this.proxy.getChoicesAsStringForUI(t => {
+ parameterElement.value = JSON.stringify(t.responseObject());
+ });
} else if (parameterElement.id.indexOf('formattedHtml_') > -1) { // handle formatted HTML
- await new Promise(resolve => this.proxy.getChoicesAsStringForUI(t => {
+ await this.proxy.getChoicesAsStringForUI(t => {
parameterElement.innerHTML = t.responseObject();
- resolve(t)
- }));
+ });
}
// propagate change
// console.log('Propagating change event from ' + this.getParameterName());
// let e1 = $.Event('change', {parameterName: this.getParameterName()});
- // $(this.getParameterElement()).trigger(e1);
+ // jQuery3(this.getParameterElement()).trigger(e1);
if (!avoidRecursion) {
if (cascadeParameters && cascadeParameters.length > 0) {
for (let i = 0; i < cascadeParameters.length; i++) {
@@ -542,21 +537,21 @@ var UnoChoice = UnoChoice || ($ => {
this.originalArray = [];
// push existing values into originalArray array
if (this.paramElement.tagName === 'SELECT') { // handle SELECTS
- let options = $(paramElement).children().toArray();
+ let options = jQuery3(paramElement).children().toArray();
for (let i = 0; i < options.length; ++i) {
this.originalArray.push(options[i]);
}
} else if (paramElement.tagName === 'DIV' || paramElement.tagName === 'SPAN') { // handle CHECKBOXES
- if ($(paramElement).children().length > 0 && (paramElement.children[0].tagName === 'DIV' || paramElement.children[0].tagName === 'SPAN')) {
+ if (jQuery3(paramElement).children().length > 0 && (paramElement.children[0].tagName === 'DIV' || paramElement.children[0].tagName === 'SPAN')) {
let tbody = paramElement.children[0];
- let trs = $(tbody).find('div');
+ let trs = jQuery3(tbody).find('div');
for (let i = 0; i < trs.length ; ++i) {
- let tds = $(trs[i]).find('div');
- let inputs = $(tds[0]).find('input');
+ let tds = jQuery3(trs[i]).find('div');
+ let inputs = jQuery3(tds[0]).find('input');
let input = inputs[0];
this.originalArray.push(input);
}
- } // if ($(paramElement).children().length > 0 && paramElement.children[0].tagName === 'DIV') {
+ } // if (jQuery3(paramElement).children().length > 0 && paramElement.children[0].tagName === 'DIV') {
}
this.initEventHandler();
}
@@ -615,7 +610,7 @@ var UnoChoice = UnoChoice || ($ => {
*/
FilterElement.prototype.initEventHandler = function() {
let _self = this;
- $(_self.filterElement).keyup(e => {
+ jQuery3(_self.filterElement).keyup(e => {
//let filterElement = e.target;
let filterElement = _self.getFilterElement();
let filteredElement = _self.getParameterElement();
@@ -646,17 +641,17 @@ var UnoChoice = UnoChoice || ($ => {
let tagName = filteredElement.tagName;
if (tagName === 'SELECT') { // handle SELECT's
- $(filteredElement).children().remove();
+ jQuery3(filteredElement).children().remove();
for (let i = 0; i < newOptions.length ; ++i) {
let opt = document.createElement('option');
opt.value = newOptions[i].value;
opt.innerHTML = newOptions[i].innerHTML;
- $(filteredElement).append(opt);
+ jQuery3(filteredElement).append(opt);
}
} else if (tagName === 'DIV' || tagName === 'SPAN') { // handle CHECKBOXES, RADIOBOXES and other elements (Jenkins renders them as tables)
- if ($(filteredElement).children().length > 0 && ($(filteredElement).children()[0].tagName === 'DIV' || $(filteredElement).children()[0].tagName === 'SPAN')) {
+ if (jQuery3(filteredElement).children().length > 0 && (jQuery3(filteredElement).children()[0].tagName === 'DIV' || jQuery3(filteredElement).children()[0].tagName === 'SPAN')) {
let tbody = filteredElement.children[0];
- $(tbody).empty();
+ jQuery3(tbody).empty();
if (filteredElement.className === 'dynamic_checkbox') {
for (let i = 0; i < newOptions.length; i++) {
let entry = newOptions[i];
@@ -707,12 +702,12 @@ var UnoChoice = UnoChoice || ($ => {
tbody.appendChild(tr);
}
}
- } // if ($(filteredElement).children().length > 0 && $(filteredElement).children()[0].tagName === 'DIV') {
+ } // if (jQuery3(filteredElement).children().length > 0 && jQuery3(filteredElement).children()[0].tagName === 'DIV') {
} // if (tagName === 'SELECT') { // } else if (tagName === 'DIV') {
// Propagate the changes made by the filter
console.log('Propagating change event after filtering');
let e1 = $.Event('change', {parameterName: 'Filter Element Event'});
- $(filteredElement).trigger(e1);
+ jQuery3(filteredElement).trigger(e1);
});
}
// HTML utility methods
@@ -736,11 +731,11 @@ var UnoChoice = UnoChoice || ($ => {
* @see issue #21 in GitHub - github.com/biouno/uno-choice-plugin/issues
*/
function fakeSelectRadioButton(clazzName, id) {
- let element = $(`#${id}`).get(0);
+ let element = jQuery3(`#${id}`).get(0);
// deselect all radios with the class=clazzName
- let radios = $(`input[class="${clazzName}"]`);
+ let radios = jQuery3(`input[class="${clazzName}"]`);
radios.each(function(index) {
- $(this).attr('name', '');
+ jQuery3(this).attr('name', '');
});
// select the radio with the id=id
let parent = element.parentNode;
@@ -768,7 +763,7 @@ var UnoChoice = UnoChoice || ($ => {
* @return {string} the value of the HTML element used as parameter value in Jenkins, as a string
*/
function getParameterValue(htmlParameter) {
- let e = $(htmlParameter);
+ let e = jQuery3(htmlParameter);
let value = '';
if (e.attr('name') === 'value') {
value = util.getElementValue(e);
@@ -777,7 +772,7 @@ var UnoChoice = UnoChoice || ($ => {
if (subElements) {
let valueBuffer = Array();
subElements.each(function() {
- let tempValue = util.getElementValue($(this));
+ let tempValue = util.getElementValue(jQuery3(this));
if (tempValue)
valueBuffer.push(tempValue);
});
@@ -795,8 +790,101 @@ var UnoChoice = UnoChoice || ($ => {
return value;
}
+ // Hacks in Jenkins core
+ /**
+ * This function is the same as makeStaplerProxy available in Jenkins core, but executes calls
+ * synchronously. Since many parameters must be filled only after other parameters have been
+ * updated, calling Jenkins methods asynchronously causes several unpredictable errors.
+ *
+ * JENKINS-71909: Stapler had to be updated when Prototype and jQuery dependencies
+ * were removed from Jenkins. This means that we also had to update this function to
+ * match what was done there - thanks asc3ns10n (GH).
+ *
+ * @param url {string} The URL
+ * @param staplerCrumb {string} The crumb
+ * @param methods {Array} The methods
+ */
+ function makeStaplerProxy2(url, staplerCrumb, methods) {
+ if (url.substring(url.length - 1) !== '/') url+='/';
+ let proxy = {};
+ let stringify;
+ if (Object.toJSON) // needs to use Prototype.js if it's present. See commit comment for discussion
+ stringify = Object.toJSON; // from prototype
+ else if (typeof(JSON)=="object" && JSON.stringify)
+ stringify = JSON.stringify; // standard
+ let genMethod = methodName => {
+ proxy[methodName] = async function() {
+ let args = arguments;
+ // the final argument can be a callback that receives the return value
+ let callback = (() => {
+ if (args.length === 0) return null;
+ let tail = args[args.length-1];
+ return (typeof(tail)=='function') ? tail : null;
+ })();
+ // 'arguments' is not an array, so we convert it into an array
+ let a = [];
+ for (let i=0; i {
async function renderCascadeChoiceParameter(parentDivRef, filterable, name, randomName, filterLength, paramName, referencedParameters, cascadeChoiceParameter) {
// find the cascade parameter element
- let parentDiv = jQuery(parentDivRef);
+ let parentDiv = jQuery3(parentDivRef);
let parameterHtmlElement = parentDiv.find('DIV');
if (!parameterHtmlElement || parameterHtmlElement.length === 0) {
console.log('Could not find element by name, perhaps it is a DIV?');
@@ -838,10 +926,10 @@ var UnoChoice = UnoChoice || ($ => {
for (let i = 0; i < referencedParameters.length ; ++i) {
let parameterElement = null;
// FIXME: review the block below
- let divs = jQuery('div[name="parameter"]');
+ let divs = jQuery3('div[name="parameter"]');
for (let j = 0; j < divs.length ; j++) {
let div = divs[j];
- let hiddenNames = jQuery(div).find('input[name="name"]');
+ let hiddenNames = jQuery3(div).find('input[name="name"]');
if (hiddenNames[0].value === referencedParameters[i]) {
let children = div.children;
for (let k = 0; k < children.length; ++k) {
@@ -850,7 +938,7 @@ var UnoChoice = UnoChoice || ($ => {
parameterElement = child;
break;
} else if (child.tagName === 'DIV' || child.tagName === 'SPAN') {
- let subValues = jQuery(child).find('input[name="value"]');
+ let subValues = jQuery3(child).find('input[name="value"]');
if (subValues && subValues.get(0)) {
parameterElement = child;
break;
@@ -882,36 +970,36 @@ var UnoChoice = UnoChoice || ($ => {
async function renderDynamicRenderParameter(parentDivRef, name, paramName, referencedParameters, dynamicReferenceParameter) {
// find the cascade parameter element
- let parentDiv = jQuery(parentDivRef);
+ let parentDiv = jQuery3(parentDivRef);
// if the parameter class has been set to hidden, then we hide it now
if (parentDiv.get(0).getAttribute('class') === 'hidden_uno_choice_parameter') {
- let parentTbody = jQuery(parentDiv.get(0)).parents('tbody');
+ let parentTbody = jQuery3(parentDiv.get(0)).parents('tbody');
// FIXME: temporary fix to support both TABLE and DIV in the Jenkins UI
// remove after most users have migrated to newer versions with DIVs
if (!parentTbody || parentTbody.length === 0) {
- parentTbody = jQuery(parentDiv.get(0)).parents('div > div.tr');
+ parentTbody = jQuery3(parentDiv.get(0)).parents('div > div.tr');
}
if (parentTbody && parentTbody.length > 0) {
- jQuery(parentTbody.get(0)).attr('style', 'visibility:hidden;position:absolute;');
+ jQuery3(parentTbody.get(0)).attr('style', 'visibility:hidden;position:absolute;');
}
}
let parameterHtmlElement = null;
for(let i = 0; i < parentDiv.children().length; i++) {
let child = parentDiv.children()[i];
if (child.getAttribute('name') === 'value' || child.id.indexOf('ecp_') > -1) {
- parameterHtmlElement = jQuery(child);
+ parameterHtmlElement = jQuery3(child);
break;
}
if (child.id.indexOf('inputElement_') > -1) {
- parameterHtmlElement = jQuery(child);
+ parameterHtmlElement = jQuery3(child);
break;
}
if (child.id.indexOf('formattedHtml_') > -1) {
- parameterHtmlElement = jQuery(child);
+ parameterHtmlElement = jQuery3(child);
break;
}
if (child.id.indexOf('imageGallery_') > -1) {
- parameterHtmlElement = jQuery(child);
+ parameterHtmlElement = jQuery3(child);
break;
}
}
@@ -921,10 +1009,10 @@ var UnoChoice = UnoChoice || ($ => {
for (let i = 0; i < referencedParameters.length ; ++i) {
let parameterElement = null;
// FIXME: review the block below
- let divs = jQuery('div[name="parameter"]');
+ let divs = jQuery3('div[name="parameter"]');
for (let j = 0; j < divs.length ; j++) {
let div = divs[j];
- let hiddenNames = jQuery(div).find('input[name="name"]');
+ let hiddenNames = jQuery3(div).find('input[name="name"]');
if (hiddenNames[0].value === referencedParameters[i]) {
let children = div.children;
for (let k = 0; k < children.length; ++k) {
@@ -933,7 +1021,7 @@ var UnoChoice = UnoChoice || ($ => {
parameterElement = child;
break;
} else if (child.tagName === 'DIV' || child.tagName === 'SPAN') {
- let subValues = jQuery(child).find('input[name="value"]');
+ let subValues = jQuery3(child).find('input[name="value"]');
if (subValues && subValues.get(0)) {
parameterElement = child;
break;
@@ -970,6 +1058,7 @@ var UnoChoice = UnoChoice || ($ => {
instance.DynamicReferenceParameter = DynamicReferenceParameter;
instance.ReferencedParameter = ReferencedParameter;
instance.FilterElement = FilterElement;
+ instance.makeStaplerProxy2 = makeStaplerProxy2;
instance.cascadeParameters = cascadeParameters;
instance.renderChoiceParameter = renderChoiceParameter;
instance.renderCascadeChoiceParameter = renderCascadeChoiceParameter;
diff --git a/src/test/java/org/biouno/unochoice/BaseUiTest.java b/src/test/java/org/biouno/unochoice/BaseUiTest.java
new file mode 100644
index 00000000..f28b4540
--- /dev/null
+++ b/src/test/java/org/biouno/unochoice/BaseUiTest.java
@@ -0,0 +1,176 @@
+package org.biouno.unochoice;
+
+import io.github.bonigarcia.wdm.WebDriverManager;
+import org.apache.commons.lang3.StringUtils;
+import org.htmlunit.ElementNotFoundException;
+import org.jfree.data.category.CategoryRangeInfo;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.StaleElementReferenceException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import java.text.MessageFormat;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public abstract class BaseUiTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ protected WebDriver driver;
+ protected WebDriverWait wait;
+
+ protected static boolean isCi() {
+ return StringUtils.isNotBlank(System.getenv("CI"));
+ }
+
+ protected static final Duration MAX_WAIT = Duration.parse(System.getProperty("ui.loading.timeout", "PT300S"));
+
+ @BeforeClass
+ public static void setUpClass() {
+ if (isCi()) {
+ // The browserVersion needs to match what is provided by the Jenkins Infrastructure
+ // If you see an exception like this:
+ //
+ // org.openqa.selenium.SessionNotCreatedException: Could not start a new session. Response code 500. Message: session not created: This version of ChromeDriver only supports Chrome version 114
+ // Current browser version is 112.0.5615.49 with binary path /usr/bin/chromium-browser
+ //
+ // Then that means you need to update the version here to match the current browser version.
+ WebDriverManager.chromedriver().browserVersion("112").setup();
+ } else {
+ WebDriverManager.chromedriver().setup();
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ if (isCi()) {
+ driver = new ChromeDriver(new ChromeOptions().addArguments("--headless", "--disable-dev-shm-usage", "--no-sandbox"));
+ } else {
+ driver = new ChromeDriver(new ChromeOptions());
+ }
+ wait = new WebDriverWait(driver, MAX_WAIT);
+ driver.manage().window().setSize(new Dimension(2560, 1440));
+ }
+
+ @After
+ public void tearDown() {
+ driver.quit();
+ }
+
+ protected static By radios(String paramName) {
+ return By.cssSelector("div.active-choice:has([name='name'][value='" + paramName + "']) input[type='radio']");
+ }
+
+ protected List findRadios(String paramName) {
+ return findRadios(paramName, null);
+ }
+
+ protected List findRadios(String paramName, Map attributes) {
+ return driver.findElements(radios(paramName));
+ }
+
+ protected static By checkboxes(String paramName) {
+ return By.cssSelector("div.active-choice:has([name='name'][value='" + paramName + "']) input[type='checkbox']");
+ }
+
+ protected List findCheckboxes(String paramName) {
+ return driver.findElements(checkboxes(paramName));
+ }
+
+ protected static By selects(String paramName) {
+ return By.cssSelector("div.active-choice:has([name='name'][value='" + paramName + "']) > select");
+ }
+
+ protected WebElement findSelect(String paramName) {
+ return driver.findElement(selects(paramName));
+ }
+
+ protected WebElement findParamDiv(String paramName) {
+ final WebElement paramValueInput = driver.findElement(By.cssSelector("input[name='parameter.name'][value='" + paramName + "']"));
+ // Up to how many parent levels to we want to search for the help button?
+ // At the moment it's 3 levels up, so let's give it some room, use 7.
+ final int parentsLimit = 7;
+ WebElement parentElement = paramValueInput.findElement(By.xpath("./.."));
+ for (int i = 0; i < parentsLimit; i++) {
+ if (parentElement.getAttribute("name") != null && parentElement.getAttribute("name").equals("parameterDefinitions")) {
+ return parentElement;
+ }
+ parentElement = parentElement.findElement(By.xpath("./.."));
+ }
+ throw new ElementNotFoundException("div", "parameterDefinitions", "");
+ }
+
+ protected void checkOptions(Supplier param1Input, String... options) {
+ wait.withMessage(() -> {
+ List optionElements = param1Input.get().findElements(By.cssSelector("option"));
+ List optionValues = optionElements.stream().map(WebElement::getText).collect(Collectors.toList());
+ return MessageFormat.format("{0} should have had {1}. Had {2}", param1Input, Arrays.asList(options), optionValues);
+ })
+ .until(d -> {
+ try {
+ List optionElements = param1Input.get().findElements(By.cssSelector("option"));
+ List optionValues = optionElements.stream().map(WebElement::getText).collect(Collectors.toList());
+ return optionValues.equals(Arrays.asList(options));
+ } catch (StaleElementReferenceException e) {
+ return false;
+ }
+ });
+ }
+
+ /**
+ * This function receives a {@code By} selector to avoid stale elements - it will repeatedly
+ * query the driver for a new element.
+ *
+ * @param selector selector
+ * @param options expected options
+ */
+ protected void checkRadios(By selector, String... options) {
+ wait.withMessage(() -> {
+ final List radios = driver.findElements(selector);
+ List optionValues = radios.stream().map(it -> it.getAttribute("value")).collect(Collectors.toList());
+ return MessageFormat.format("{0} should have had {1}. Had {2}", radios, Arrays.asList(options), optionValues);
+ })
+ .until(d -> {
+ try {
+ final List radios = driver.findElements(selector);
+ List optionValues = radios.stream().map(it -> it.getAttribute("value")).collect(Collectors.toList());
+ return optionValues.equals(Arrays.asList(options));
+ } catch (StaleElementReferenceException e) {
+ return false;
+ }
+ });
+ }
+
+ protected void checkRadios(Supplier> radios, String... options) {
+ wait.withMessage(() -> {
+ List optionValues = radios.get().stream().map(it -> it.getAttribute("value")).collect(Collectors.toList());
+ return MessageFormat.format("{0} should have had {1}. Had {2}", radios, Arrays.asList(options), optionValues);
+ })
+ .until(d -> {
+ try {
+ List optionValues = radios.get().stream().map(it -> it.getAttribute("value")).collect(Collectors.toList());
+ return optionValues.equals(Arrays.asList(options));
+ } catch (StaleElementReferenceException e) {
+ return false;
+ }
+ });
+ }
+}
diff --git a/src/test/java/org/biouno/unochoice/UiAcceptanceTest.java b/src/test/java/org/biouno/unochoice/UiAcceptanceTest.java
index 94cd6c21..58993fc2 100644
--- a/src/test/java/org/biouno/unochoice/UiAcceptanceTest.java
+++ b/src/test/java/org/biouno/unochoice/UiAcceptanceTest.java
@@ -1,82 +1,20 @@
package org.biouno.unochoice;
-import io.github.bonigarcia.wdm.WebDriverManager;
-import org.apache.commons.lang3.StringUtils;
-import org.htmlunit.ElementNotFoundException;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
import org.junit.Test;
-import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;
import org.openqa.selenium.By;
-import org.openqa.selenium.Dimension;
-import org.openqa.selenium.StaleElementReferenceException;
-import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
-import org.openqa.selenium.chrome.ChromeDriver;
-import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
-import org.openqa.selenium.support.ui.WebDriverWait;
-import java.text.MessageFormat;
-import java.time.Duration;
-import java.util.Arrays;
import java.util.List;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-public class UiAcceptanceTest {
-
- private static final Duration MAX_WAIT = Duration.parse(System.getProperty("ui.loading.timeout", "PT60S"));
-
- @Rule
- public JenkinsRule j = new JenkinsRule();
-
- private WebDriver driver;
- private WebDriverWait wait;
-
- @BeforeClass
- public static void setUpClass() {
- if (isCi()) {
- // The browserVersion needs to match what is provided by the Jenkins Infrastructure
- // If you see an exception like this:
- //
- // org.openqa.selenium.SessionNotCreatedException: Could not start a new session. Response code 500. Message: session not created: This version of ChromeDriver only supports Chrome version 114
- // Current browser version is 112.0.5615.49 with binary path /usr/bin/chromium-browser
- //
- // Then that means you need to update the version here to match the current browser version.
- WebDriverManager.chromedriver().browserVersion("112").setup();
- } else {
- WebDriverManager.chromedriver().setup();
- }
- }
-
- private static boolean isCi() {
- return StringUtils.isNotBlank(System.getenv("CI"));
- }
-
- @Before
- public void setUp() throws Exception {
- if (isCi()) {
- driver = new ChromeDriver(new ChromeOptions().addArguments("--headless", "--disable-dev-shm-usage", "--no-sandbox"));
- } else {
- driver = new ChromeDriver(new ChromeOptions());
- }
- wait = new WebDriverWait(driver, MAX_WAIT);
- driver.manage().window().setSize(new Dimension(2560, 1440));
- }
-
- @After
- public void tearDown() {
- driver.quit();
- }
+public class UiAcceptanceTest extends BaseUiTest {
@LocalData("test")
@Test
@@ -92,7 +30,11 @@ public void testHelpFiles() throws Exception {
final WebElement helpIcon = param1ParamDiv.findElement(By.cssSelector("a.jenkins-help-button"));
wait.until(ExpectedConditions.elementToBeClickable(helpIcon));
- helpIcon.click();
+ Actions actions = new Actions(driver);
+ actions
+ .moveToElement(helpIcon)
+ .click()
+ .perform();
wait.withMessage(() -> "The help text should have been displayed").until(d -> helpTextDiv.isDisplayed());
@@ -215,63 +157,4 @@ public void test() throws Exception {
checkOptions(() -> findSelect("PARAM4A"));
}
-
- private List findRadios(String paramName) {
- return driver.findElements(By.cssSelector("div.active-choice:has([name='name'][value='" + paramName + "']) input[type='radio']"));
- }
-
- private List findCheckboxes(String paramName) {
- return driver.findElements(By.cssSelector("div.active-choice:has([name='name'][value='" + paramName + "']) input[type='checkbox']"));
- }
-
- private WebElement findSelect(String paramName) {
- return driver.findElement(By.cssSelector("div.active-choice:has([name='name'][value='" + paramName + "']) > select"));
- }
-
- private WebElement findParamDiv(String paramName) {
- final WebElement paramValueInput = driver.findElement(By.cssSelector("input[name='parameter.name'][value='" + paramName + "']"));
- // Up to how many parent levels to we want to search for the help button?
- // At the moment it's 3 levels up, so let's give it some room, use 7.
- final int parentsLimit = 7;
- WebElement parentElement = paramValueInput.findElement(By.xpath("./.."));
- for (int i = 0; i < parentsLimit; i++) {
- if (parentElement.getAttribute("name") != null && parentElement.getAttribute("name").equals("parameterDefinitions")) {
- return parentElement;
- }
- parentElement = parentElement.findElement(By.xpath("./.."));
- }
- throw new ElementNotFoundException("div", "parameterDefinitions", "");
- }
-
- private void checkOptions(Supplier param1Input, String... options) {
- wait.withMessage(() -> {
- List optionElements = param1Input.get().findElements(By.cssSelector("option"));
- List optionValues = optionElements.stream().map(WebElement::getText).collect(Collectors.toList());
- return MessageFormat.format("{0} should have had {1}. Had {2}", param1Input, Arrays.asList(options), optionValues);
- })
- .until(d -> {
- try {
- List optionElements = param1Input.get().findElements(By.cssSelector("option"));
- List optionValues = optionElements.stream().map(WebElement::getText).collect(Collectors.toList());
- return optionValues.equals(Arrays.asList(options));
- } catch (StaleElementReferenceException e) {
- return false;
- }
- });
- }
-
- private void checkRadios(Supplier> radios, String... options) {
- wait.withMessage(() -> {
- List optionValues = radios.get().stream().map(it -> it.getAttribute("value")).collect(Collectors.toList());
- return MessageFormat.format("{0} should have had {1}. Had {2}", radios, Arrays.asList(options), optionValues);
- })
- .until(d -> {
- try {
- List optionValues = radios.get().stream().map(it -> it.getAttribute("value")).collect(Collectors.toList());
- return optionValues.equals(Arrays.asList(options));
- } catch (StaleElementReferenceException e) {
- return false;
- }
- });
- }
}
diff --git a/src/test/java/org/biouno/unochoice/issue62835/TestForNodeLabelParameter.java b/src/test/java/org/biouno/unochoice/issue62835/TestForNodeLabelParameter.java
index 14bd8b68..4f2d712c 100644
--- a/src/test/java/org/biouno/unochoice/issue62835/TestForNodeLabelParameter.java
+++ b/src/test/java/org/biouno/unochoice/issue62835/TestForNodeLabelParameter.java
@@ -57,7 +57,6 @@
import com.google.common.collect.Lists;
import hudson.model.FreeStyleProject;
-import hudson.model.ParameterDefinition;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.labels.LabelAtom;
import hudson.slaves.DumbSlave;
@@ -117,30 +116,31 @@ public void testNodeLabelParameterValueFound() throws IOException, SAXException
true,
1);
- project.addProperty(new ParametersDefinitionProperty(Arrays.asList(nodeLabelParameter, reactsToNodeLabelParameter)));
+ project.addProperty(new ParametersDefinitionProperty(Arrays.asList(nodeLabelParameter, reactsToNodeLabelParameter)));
project.save();
- WebClient wc = j.createWebClient();
- wc.setThrowExceptionOnFailingStatusCode(false);
- HtmlPage configPage = wc.goTo("job/" + project.getName() + "/build?delay=0sec");
- DomElement renderedParameterElement = configPage.getElementById("random-name");
- HtmlSelect select = null;
- for (DomNode node: renderedParameterElement.getChildren()) {
- if (node instanceof HtmlSelect) {
- select = (HtmlSelect) node;
- break;
+ try (WebClient wc = j.createWebClient()) {
+ wc.setThrowExceptionOnFailingStatusCode(false);
+ HtmlPage configPage = wc.goTo("job/" + project.getName() + "/build?delay=0sec");
+ DomElement renderedParameterElement = configPage.getElementById("random-name");
+ HtmlSelect select = null;
+ for (DomNode node: renderedParameterElement.getChildren()) {
+ if (node instanceof HtmlSelect) {
+ select = (HtmlSelect) node;
+ break;
+ }
}
+ if (select == null) {
+ fail("Missing cascade parameter select HTML node element!");
+ }
+ List htmlOptions = select.getOptions();
+ final List options = htmlOptions
+ .stream()
+ .map(HtmlOption::getText)
+ .collect(Collectors.toList());
+ final List expected = new LinkedList<>(Collections.singletonList(nodeName));
+ assertEquals("Wrong number of HTML options rendered", expected.size(), options.size());
+ assertEquals("Wrong HTML options rendered (or out of order)", expected, options);
}
- if (select == null) {
- fail("Missing cascade parameter select HTML node element!");
- }
- List htmlOptions = select.getOptions();
- final List options = htmlOptions
- .stream()
- .map(HtmlOption::getText)
- .collect(Collectors.toList());
- final List expected = new LinkedList<>(Collections.singletonList(nodeName));
- assertEquals("Wrong number of HTML options rendered", expected.size(), options.size());
- assertEquals("Wrong HTML options rendered (or out of order)", expected, options);
}
}
diff --git a/src/test/java/org/biouno/unochoice/issue71909/TestRevertingAsynchronousProxy.java b/src/test/java/org/biouno/unochoice/issue71909/TestRevertingAsynchronousProxy.java
new file mode 100644
index 00000000..63576335
--- /dev/null
+++ b/src/test/java/org/biouno/unochoice/issue71909/TestRevertingAsynchronousProxy.java
@@ -0,0 +1,87 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2024 Ioannis Moutsatsos, Bruno P. Kinoshita
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.biouno.unochoice.issue71909;
+
+import org.biouno.unochoice.BaseUiTest;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.recipes.LocalData;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.Select;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * See JENKINS-71909.
+ *
+ * JENKINS-71365 made Stapler proxy asynchronous and used JavaScript promises. This
+ * caused a regression where users could no longer render parameters in a deterministic
+ * way.
+ *
+ * To have proper asynchronous reactivity, we would have to either implement something
+ * akin to Vue or React's reactivity engine, since users would have to be able to declare
+ * how parameters react based on more elaborated constraints.
+ *
+ * This test uses the example from JENKINS-71909 to reproduce the bug.
+ *
+ * @since 2.8.4
+ */
+@Issue("JENKINS-71909")
+public class TestRevertingAsynchronousProxy extends BaseUiTest {
+
+ @LocalData("test")
+ @Test
+ public void test() throws Exception {
+ // Load the page
+ driver.get(j.getURL().toString() + "job/test/build");
+
+ // From OP:
+ //
+ // The issue is when "Item2" is selected, then the 4) active-choice
+ // elements returns as selected "buster" instead of "bullseye", this
+ // because, when first called the wrong server list is returned from
+ // 3), because 3) is called with "a1" as parameter instead of "a2"
+ // as should be after the 2) gets executed.
+
+ WebElement targetParam = findSelect("TARGET");
+ assertTrue(targetParam.isDisplayed());
+ assertTrue(targetParam.isEnabled());
+ new Select(targetParam).selectByValue("Item2");
+
+ wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".jenkins-spinner")));
+
+ List dockerBaseImageParam = findRadios("DOCKER_BASE_IMAGE");
+ assertEquals(2, dockerBaseImageParam.size());
+
+ checkRadios(radios("DOCKER_BASE_IMAGE"), "buster", "bullseye");
+
+ assertEquals("bullseye", findRadios("DOCKER_BASE_IMAGE").get(1).getAttribute("value"));
+ assertEquals("true", findRadios("DOCKER_BASE_IMAGE").get(1).getAttribute("checked"));
+ }
+}
diff --git a/src/test/java/org/biouno/unochoice/issue71909/package-info.java b/src/test/java/org/biouno/unochoice/issue71909/package-info.java
new file mode 100644
index 00000000..ec96e2ba
--- /dev/null
+++ b/src/test/java/org/biouno/unochoice/issue71909/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014-2024 Ioannis Moutsatsos, Bruno P. Kinoshita
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * See JENKINS-71909.
+ *
+ * @since 2.8.4
+ */
+package org.biouno.unochoice.issue71909;
diff --git a/src/test/resources/org/biouno/unochoice/issue71909/TestRevertingAsynchronousProxy/test/jobs/test/config.xml b/src/test/resources/org/biouno/unochoice/issue71909/TestRevertingAsynchronousProxy/test/jobs/test/config.xml
new file mode 100644
index 00000000..c24ea601
--- /dev/null
+++ b/src/test/resources/org/biouno/unochoice/issue71909/TestRevertingAsynchronousProxy/test/jobs/test/config.xml
@@ -0,0 +1,133 @@
+
+
+
+
+ false
+
+
+
+
+ TARGET
+ choice-parameter-19685286034459
+ 1
+
+ false
+
+
+
+ false
+
+
+ JENKINS-71909
+ JENKINS-71909
+ PT_SINGLE_SELECT
+ false
+ 1
+
+
+ VARIANT
+ choice-parameter-19685291017751
+ 1
+
+ false
+
+
+
+ false
+
+
+ JENKINS-71909
+ JENKINS-71909
+
+ TARGET
+ PT_RADIO
+ false
+ 1
+
+
+ MACHINES
+ choice-parameter-19685294460278
+ 1
+
+ false
+
+
+
+ false
+
+
+ JENKINS-71909
+ JENKINS-71909
+
+ TARGET,VARIANT
+ PT_CHECKBOX
+ false
+ 1
+
+
+ DOCKER_BASE_IMAGE
+ choice-parameter-19685297716936
+ 1
+
+ false
+
+
+
+ false
+
+
+ JENKINS-71909
+ JENKINS-71909
+
+ MACHINES
+ PT_RADIO
+ false
+ 1
+
+
+
+
+
+ true
+ false
+ false
+ false
+
+ false
+
+
+
+