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..95b9b966 --- /dev/null +++ b/src/test/java/org/biouno/unochoice/BaseUiTest.java @@ -0,0 +1,195 @@ +/* + * 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; + +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.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.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +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..1ff27974 100644 --- a/src/test/java/org/biouno/unochoice/UiAcceptanceTest.java +++ b/src/test/java/org/biouno/unochoice/UiAcceptanceTest.java @@ -1,82 +1,43 @@ +/* + * 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; -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 +53,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 +180,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 + + + +