fix: prefix form arguments to prevent collisions with other page elem… (#1072)

This commit is contained in:
James Read 2026-07-14 19:49:10 +01:00 committed by GitHub
commit ee97906808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 398 additions and 67 deletions

View File

@ -10,7 +10,7 @@
"stylelint-config-standard": "^40.0.0"
},
"scripts": {
"test": "node --test resources/vue/components/*.test.mjs"
"test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs"
},
"author": "",
"parcelIgnore": [

View File

@ -1,5 +1,5 @@
<template>
<div class="choice-checklist" :id="`${id}-wrapper`">
<div class="choice-checklist" :id="wrapperId">
<div class="choice-checklist-controls">
<button type="button" class="choice-checklist-control" @click="selectAll">
Select all
@ -14,10 +14,10 @@
v-for="(choice, index) in choices"
:key="choice.value"
class="choice-checklist-item"
:for="`${id}-${index}`"
:for="optionId(index)"
>
<input
:id="`${id}-${index}`"
:id="optionId(index)"
type="checkbox"
:checked="isSelected(choice.value)"
@change="handleToggle(choice.value)"
@ -26,7 +26,7 @@
</label>
</fieldset>
<input
:id="`${id}-value`"
:id="valueId"
:name="name"
type="text"
class="visually-hidden choice-checklist-value"
@ -47,12 +47,13 @@ import {
parseChecklistValue,
toggleChoice
} from '../utils/choiceChecklistHelpers.js'
import {
argumentFieldOptionId,
argumentFieldValueId,
argumentFieldWrapperId
} from '../utils/argumentFieldIds.js'
const props = defineProps({
id: {
type: String,
required: true
},
name: {
type: String,
required: true
@ -78,6 +79,12 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const selectedValues = computed(() => parseChecklistValue(props.modelValue))
const wrapperId = computed(() => argumentFieldWrapperId(props.name))
const valueId = computed(() => argumentFieldValueId(props.name))
function optionId (index) {
return argumentFieldOptionId(props.name, index)
}
function isSelected(value) {
return selectedValues.value.includes(value)

View File

@ -31,7 +31,7 @@
>
<li
v-for="(choice, index) in filteredChoices"
:id="`${listboxId}-option-${index}`"
:id="listboxOptionId(index)"
:key="choice.value"
role="option"
:aria-selected="choice.value === modelValue"
@ -56,6 +56,10 @@ import {
choiceDisplayLabel,
syncStateFromModelValue
} from './choiceComboboxHelpers.js'
import {
argumentFieldListboxId,
argumentFieldListboxOptionId
} from '../utils/argumentFieldIds.js'
const props = defineProps({
id: {
@ -91,16 +95,20 @@ const query = ref('')
const isUserFiltering = ref(false)
const highlightedIndex = ref(0)
const listboxId = computed(() => `${props.id}-listbox`)
const listboxId = computed(() => argumentFieldListboxId(props.name))
const activeDescendantId = computed(() => {
if (!isOpen.value || filteredChoices.value.length === 0) {
return undefined
}
return `${listboxId.value}-option-${highlightedIndex.value}`
return listboxOptionId(highlightedIndex.value)
})
function listboxOptionId (index) {
return argumentFieldListboxOptionId(props.name, index)
}
const placeholderText = computed(() => {
if (props.required) {
return 'Search and select...'

View File

@ -0,0 +1,41 @@
// Role-prefixed IDs keep related elements from colliding when argument names
// contain another argument's name plus a former suffix (e.g. "foo" vs "foo-choices").
const ARGUMENT_ID_NAMESPACE = 'arg-'
export const ARGUMENT_FIELD_ID_PREFIX = `${ARGUMENT_ID_NAMESPACE}field-`
export function argumentFieldId (argumentName) {
return `${ARGUMENT_FIELD_ID_PREFIX}${argumentName}`
}
export function argumentFieldChoicesId (argumentName) {
return `${ARGUMENT_ID_NAMESPACE}choices-${argumentName}`
}
export function argumentFieldValueId (argumentName) {
return `${ARGUMENT_ID_NAMESPACE}value-${argumentName}`
}
export function argumentFieldWrapperId (argumentName) {
return `${ARGUMENT_ID_NAMESPACE}wrapper-${argumentName}`
}
export function argumentFieldOptionId (argumentName, index) {
return `${ARGUMENT_ID_NAMESPACE}option-${argumentName}-${index}`
}
export function argumentFieldListboxId (argumentName) {
return `${ARGUMENT_ID_NAMESPACE}listbox-${argumentName}`
}
export function argumentFieldListboxOptionId (argumentName, index) {
return `${ARGUMENT_ID_NAMESPACE}listbox-option-${argumentName}-${index}`
}
export function argumentFieldValidationElementId (argument) {
if (argument.type === 'checklist') {
return argumentFieldValueId(argument.name)
}
return argumentFieldId(argument.name)
}

View File

@ -0,0 +1,85 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
ARGUMENT_FIELD_ID_PREFIX,
argumentFieldChoicesId,
argumentFieldId,
argumentFieldListboxId,
argumentFieldListboxOptionId,
argumentFieldOptionId,
argumentFieldValidationElementId,
argumentFieldValueId,
argumentFieldWrapperId
} from './argumentFieldIds.js'
test('argumentFieldId namespaces argument names with a field role', () => {
assert.equal(argumentFieldId('content'), 'arg-field-content')
assert.equal(argumentFieldId('banner'), 'arg-field-banner')
assert.equal(argumentFieldId('confirm'), 'arg-field-confirm')
})
test('role-prefixed ids avoid collisions between related argument elements', () => {
assert.notEqual(argumentFieldId('content'), argumentFieldChoicesId('content'))
assert.notEqual(argumentFieldId('content-choices'), argumentFieldChoicesId('content'))
assert.notEqual(argumentFieldId('segments-value'), argumentFieldValueId('segments'))
assert.notEqual(argumentFieldId('segments-wrapper'), argumentFieldWrapperId('segments'))
assert.notEqual(argumentFieldId('segments-0'), argumentFieldOptionId('segments', 0))
assert.notEqual(argumentFieldId('host-listbox'), argumentFieldListboxId('host'))
assert.notEqual(
argumentFieldId('host-listbox-option-0'),
argumentFieldListboxOptionId('host', 0)
)
})
test('argumentFieldChoicesId uses a distinct choices role', () => {
assert.equal(argumentFieldChoicesId('content'), 'arg-choices-content')
})
test('argumentFieldValidationElementId uses checklist value id', () => {
assert.equal(
argumentFieldValidationElementId({ name: 'segments', type: 'checklist' }),
'arg-value-segments'
)
})
test('argumentFieldValidationElementId uses field id for other argument types', () => {
assert.equal(
argumentFieldValidationElementId({ name: 'content', type: 'raw_string_multiline' }),
'arg-field-content'
)
assert.equal(
argumentFieldValidationElementId({ name: 'datetime', type: 'datetime' }),
'arg-field-datetime'
)
})
test('namespaced ids avoid known app-shell element ids', () => {
const appShellIds = [
'content',
'banner',
'layout',
'mainnav',
'app',
'big-error',
'available-version',
'link-login',
'username-text',
'theme-style',
'olivetin-custom-js',
'justification',
'username',
'password',
'connection-banner',
'execution-results-popup',
'logs-filter-suggestions',
'argument-popup'
]
for (const shellId of appShellIds) {
assert.notEqual(argumentFieldId(shellId), shellId)
assert.ok(argumentFieldId(shellId).startsWith(ARGUMENT_FIELD_ID_PREFIX))
assert.notEqual(argumentFieldChoicesId(shellId), shellId)
assert.notEqual(argumentFieldValueId(shellId), shellId)
}
})

View File

@ -1,13 +1,12 @@
export function readPrefilledArgumentsFromNavigation() {
const state = window.history.state
if (state?.prefilledArguments && typeof state.prefilledArguments === 'object') {
return { ...state.prefilledArguments }
export function readPrefilledArgumentsFromNavigation (historyState = globalThis.window?.history?.state) {
if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
return { ...historyState.prefilledArguments }
}
return {}
}
export function getInitialArgumentValue(paramName, prefilledArguments = {}) {
export function getInitialArgumentValue (paramName, prefilledArguments = {}, search = globalThis.window?.location?.search ?? '') {
const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
? prefilledArguments
: {}
@ -16,6 +15,6 @@ export function getInitialArgumentValue(paramName, prefilledArguments = {}) {
return safePrefilledArguments[paramName]
}
const params = new URLSearchParams(window.location.search)
const params = new URLSearchParams(search)
return params.get(paramName)
}

View File

@ -4,36 +4,31 @@ import assert from 'node:assert/strict'
import { getInitialArgumentValue, readPrefilledArgumentsFromNavigation } from './prefilledArguments.js'
test('readPrefilledArgumentsFromNavigation returns navigation state values', () => {
const originalState = window.history.state
window.history.replaceState({ prefilledArguments: { ansible_host: '10.0.0.1' } }, '')
assert.deepEqual(
readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
{ ansible_host: '10.0.0.1' }
)
})
assert.deepEqual(readPrefilledArgumentsFromNavigation(), { ansible_host: '10.0.0.1' })
window.history.replaceState(originalState, '')
test('readPrefilledArgumentsFromNavigation returns empty object when state is absent', () => {
assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
})
test('getInitialArgumentValue prefers navigation state over query params', () => {
const originalState = window.history.state
const originalSearch = window.location.search
window.history.replaceState({ prefilledArguments: { ansible_host: '10.0.0.1' } }, '')
window.history.replaceState(window.history.state, '', '?ansible_host=10.0.0.2')
assert.equal(getInitialArgumentValue('ansible_host', readPrefilledArgumentsFromNavigation()), '10.0.0.1')
window.history.replaceState(originalState, '')
window.history.replaceState(window.history.state, '', originalSearch || '/')
assert.equal(
getInitialArgumentValue(
'ansible_host',
{ ansible_host: '10.0.0.1' },
'?ansible_host=10.0.0.2'
),
'10.0.0.1'
)
})
test('getInitialArgumentValue falls back to query params when state is absent', () => {
const originalState = window.history.state
const originalSearch = window.location.search
window.history.replaceState({}, '')
window.history.replaceState(window.history.state, '', '?ansible_host=10.0.0.2')
assert.equal(getInitialArgumentValue('ansible_host', readPrefilledArgumentsFromNavigation()), '10.0.0.2')
window.history.replaceState(originalState, '')
window.history.replaceState(window.history.state, '', originalSearch || '/')
assert.equal(
getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
'10.0.0.2'
)
})

View File

@ -19,14 +19,14 @@
<template v-if="actionArguments.length > 0">
<template v-for="arg in actionArguments" :key="arg.name">
<label v-if="arg.type !== 'checklist'" :for="arg.name">
<label v-if="arg.type !== 'checklist'" :for="argumentFieldId(arg.name)">
{{ formatLabel(arg.title) }}
</label>
<div v-else class="argument-label">
{{ formatLabel(arg.title) }}
</div>
<datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="`${arg.name}-choices`">
<datalist v-if="(arg.suggestions && Object.keys(arg.suggestions).length > 0) || getBrowserSuggestions(arg).length > 0" :id="argumentFieldChoicesId(arg.name)">
<option v-for="(suggestion, key) in arg.suggestions" :key="key" :value="key">
{{ suggestion }}
</option>
@ -35,18 +35,18 @@
</option>
</datalist>
<ChoiceCombobox v-if="getInputComponent(arg) === 'select'" :id="arg.name" :name="arg.name"
<ChoiceCombobox v-if="getInputComponent(arg) === 'select'" :id="argumentFieldId(arg.name)" :name="arg.name"
:choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
@update:model-value="handleChoiceUpdate(arg, $event)" />
<ChoiceChecklist v-else-if="arg.type === 'checklist'" :id="arg.name" :name="arg.name"
<ChoiceChecklist v-else-if="arg.type === 'checklist'" :name="arg.name"
:label="arg.title" :choices="arg.choices" :model-value="getArgumentValue(arg)" :required="arg.required"
@update:model-value="handleChoiceUpdate(arg, $event)" />
<component v-else :is="getInputComponent(arg)" :id="arg.name" :name="arg.name"
<component v-else :is="getInputComponent(arg)" :id="argumentFieldId(arg.name)" :name="arg.name"
:value="(arg.type === 'checkbox' || arg.type === 'confirmation') ? undefined : getArgumentValue(arg)"
:checked="(arg.type === 'checkbox' || arg.type === 'confirmation') ? getArgumentValue(arg) : undefined"
:list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? `${arg.name}-choices` : undefined"
:list="(arg.suggestions || getBrowserSuggestions(arg).length > 0) ? argumentFieldChoicesId(arg.name) : undefined"
:type="getInputComponent(arg) !== 'select' ? getInputType(arg) : undefined"
:rows="arg.type === 'raw_string_multiline' ? 5 : undefined"
:step="arg.type === 'datetime' ? 1 : undefined" :pattern="getPattern(arg)"
@ -90,6 +90,11 @@ import {
actionRequiresJustification,
applyArgumentTemplate
} from '../utils/justificationTemplate.js'
import {
argumentFieldChoicesId,
argumentFieldId,
argumentFieldValidationElementId
} from '../utils/argumentFieldIds.js'
import { getInitialArgumentValue, readPrefilledArgumentsFromNavigation } from '../utils/prefilledArguments.js'
const router = useRouter()
@ -274,11 +279,7 @@ function handleChange(arg, event) {
}
function getValidationElement(arg) {
if (arg.type === 'checklist') {
return document.getElementById(`${arg.name}-value`)
}
return document.getElementById(arg.name)
return document.getElementById(argumentFieldValidationElementId(arg))
}
function handleChoiceUpdate(arg, value) {

View File

@ -5,6 +5,15 @@ import { Condition } from 'selenium-webdriver'
export const DEFAULT_UI_WAIT_MS = 3000
// Keep Selenium helpers in lockstep with the frontend DOM id helpers.
export {
ARGUMENT_FIELD_ID_PREFIX,
argumentFieldChoicesId,
argumentFieldId,
argumentFieldValidationElementId,
argumentFieldValueId
} from '../../frontend/resources/vue/utils/argumentFieldIds.js'
const executionDialogStatusBy = By.css('.execution-dialog-status')
export async function getActionButtons () {

View File

@ -0,0 +1,154 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, Condition } from 'selenium-webdriver'
import {
DEFAULT_UI_WAIT_MS,
argumentFieldId,
getRootAndWait,
getActionButton,
takeScreenshotOnFailure,
waitForArgumentFormPage,
waitForArgumentFormReady,
waitForLogsPage,
waitForExecutionComplete,
getTerminalBuffer,
} from '../../lib/elements.js'
async function openArgumentForm (actionTitle) {
await getRootAndWait()
const btn = await getActionButton(webdriver, actionTitle)
await btn.click()
await waitForArgumentFormPage()
await waitForArgumentFormReady()
}
async function waitForStartButtonEnabled () {
await webdriver.wait(
new Condition('wait for Start button to be enabled', async () => {
const submitButton = await webdriver.findElement(By.css('button[name="start"]'))
return await submitButton.isEnabled()
}),
DEFAULT_UI_WAIT_MS
)
}
async function waitForTerminalOutput (expectedSubstring) {
await webdriver.wait(
new Condition(`wait for terminal output containing ${expectedSubstring}`, async () => {
try {
const terminalReady = await webdriver.executeScript(`
return !!(window.terminal && window.terminal.getBufferAsString);
`)
if (!terminalReady) {
return false
}
const output = await getTerminalBuffer()
return output && output.includes(expectedSubstring)
} catch (e) {
return false
}
}),
DEFAULT_UI_WAIT_MS
)
}
describe('config: argumentIdCollision', function () {
this.timeout(10000)
before(async function () {
await runner.start('argumentIdCollision')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver)
})
it('Argument named content validates and submits (#1071)', async function () {
await openArgumentForm('Test content argument collision')
const contentWrapper = await webdriver.findElement(By.id('content'))
expect(await contentWrapper.getTagName()).to.equal('div')
const textarea = await webdriver.findElement(By.id(argumentFieldId('content')))
expect(await textarea.getTagName()).to.equal('textarea')
expect(await textarea.getAttribute('id')).to.not.equal('content')
const label = await webdriver.findElement(By.css(`label[for="${argumentFieldId('content')}"]`))
expect(await label.getText()).to.contain('Cmd input')
await textarea.sendKeys('hello from collision test')
const submitButton = await webdriver.findElement(By.css('button[name="start"]'))
await waitForStartButtonEnabled()
await submitButton.click()
await waitForLogsPage()
await waitForExecutionComplete()
await waitForTerminalOutput('Cmd input: hello from collision test')
})
it('Argument named layout validates and submits (#1071)', async function () {
await openArgumentForm('Test layout argument collision')
const layoutWrapper = await webdriver.findElement(By.id('layout'))
expect(await layoutWrapper.getTagName()).to.equal('div')
const input = await webdriver.findElement(By.id(argumentFieldId('layout')))
expect(await input.getAttribute('type')).to.equal('text')
expect(await input.getAttribute('id')).to.not.equal('layout')
await input.sendKeys('testlayoutvalue')
const submitButton = await webdriver.findElement(By.css('button[name="start"]'))
await waitForStartButtonEnabled()
await submitButton.click()
await waitForLogsPage()
await waitForExecutionComplete()
await waitForTerminalOutput('Layout value: testlayoutvalue')
})
it('Namespaced argument ids do not match app-shell ids', async function () {
await openArgumentForm('Test content argument collision')
const namespacedIds = await webdriver.executeScript(`
const requiredShellIds = ['content', 'layout'];
const optionalShellIds = ['banner', 'app', 'mainnav', 'big-error'];
const fieldId = arguments[0];
const fieldElement = document.getElementById(fieldId);
return {
fieldTag: fieldElement?.tagName?.toLowerCase() ?? null,
required: requiredShellIds.map((shellId) => {
const shellElement = document.getElementById(shellId);
return {
shellId,
shellTag: shellElement?.tagName?.toLowerCase() ?? null,
sameElement: shellElement != null && shellElement === fieldElement
};
}),
optional: optionalShellIds.map((shellId) => {
const shellElement = document.getElementById(shellId);
return {
shellId,
sameElement: shellElement != null && shellElement === fieldElement
};
})
};
`, argumentFieldId('content'))
expect(namespacedIds.fieldTag, `argument field ${argumentFieldId('content')} should exist`).to.equal('textarea')
for (const entry of namespacedIds.required) {
expect(entry.shellTag, `app-shell #${entry.shellId} should exist`).to.not.equal(null)
expect(entry.sameElement, `arg field must not resolve to #${entry.shellId}`).to.be.false
}
for (const entry of namespacedIds.optional) {
expect(entry.sameElement, `arg field must not resolve to #${entry.shellId}`).to.be.false
}
})
})

View File

@ -0,0 +1,28 @@
---
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
defaultPopupOnStart: execution-dialog
actions:
- title: Test content argument collision
exec:
- echo
- "Cmd input: {{ content }}"
icon: ping
arguments:
- name: content
title: Cmd input
type: raw_string_multiline
description: Argument name collides with the app-shell #content wrapper
- title: Test layout argument collision
exec:
- echo
- "Layout value: {{ layout }}"
icon: ping
arguments:
- name: layout
title: Layout text
type: ascii

View File

@ -10,6 +10,7 @@ import {
waitForArgumentFormPage,
waitForLogsPage,
waitForExecutionComplete,
argumentFieldId,
} from '../../lib/elements.js'
async function openCheckboxArgumentForm() {
@ -21,7 +22,7 @@ async function openCheckboxArgumentForm() {
}
async function getCheckboxInput() {
return await webdriver.findElement(By.id('confirm'))
return await webdriver.findElement(By.id(argumentFieldId('confirm')))
}
async function submitCheckboxForm() {
@ -75,7 +76,7 @@ describe('config: checkbox', function () {
expect(await checkboxInput.getTagName()).to.equal('input')
expect(await checkboxInput.getAttribute('type')).to.equal('checkbox')
const label = await webdriver.findElement(By.css('label[for="confirm"]'))
const label = await webdriver.findElement(By.css(`label[for="${argumentFieldId('confirm')}"]`))
expect(await label.getText()).to.contain('Confirm option')
})

View File

@ -7,6 +7,7 @@ import {
takeScreenshotOnFailure,
waitForArgumentFormReady,
waitForLogsPage,
argumentFieldId,
} from '../../lib/elements.js'
describe('config: datetime', function () {
@ -32,7 +33,7 @@ describe('config: datetime', function () {
await waitForArgumentFormReady()
// Find the datetime input field
const datetimeInput = await webdriver.findElement(By.id('datetime'))
const datetimeInput = await webdriver.findElement(By.id(argumentFieldId('datetime')))
// Verify it's a datetime-local input type
const inputType = await datetimeInput.getAttribute('type')
@ -43,7 +44,7 @@ describe('config: datetime', function () {
expect(step).to.equal('1', 'Step attribute should be 1')
// Verify the label is present
const label = await webdriver.findElement(By.css('label[for="datetime"]'))
const label = await webdriver.findElement(By.css(`label[for="${argumentFieldId('datetime')}"]`))
expect(await label.getText()).to.contain('Select a date and time')
})
@ -57,7 +58,7 @@ describe('config: datetime', function () {
await waitForArgumentFormReady()
// Find the datetime input field
const datetimeInput = await webdriver.findElement(By.id('datetime'))
const datetimeInput = await webdriver.findElement(By.id(argumentFieldId('datetime')))
// Set a datetime value (format: YYYY-MM-DDTHH:mm)
// datetime-local returns values without seconds, backend will add :00

View File

@ -3,6 +3,8 @@ import { expect } from 'chai'
import { By, Condition } from 'selenium-webdriver'
import {
DEFAULT_UI_WAIT_MS,
argumentFieldChoicesId,
argumentFieldId,
getRootAndWait,
getActionButton,
takeScreenshotOnFailure,
@ -73,15 +75,15 @@ async function openArgumentForm() {
}
async function getTestInput() {
return await webdriver.findElement(By.id('testInput'))
return await webdriver.findElement(By.id(argumentFieldId('testInput')))
}
async function getTestInput2() {
return await webdriver.findElement(By.id('testInput2'))
return await webdriver.findElement(By.id(argumentFieldId('testInput2')))
}
async function getDatalistOptions(inputName = 'testInput') {
return await webdriver.findElements(By.css(`datalist#${inputName}-choices option`))
return await webdriver.findElements(By.css(`datalist#${argumentFieldChoicesId(inputName)} option`))
}
async function submitForm() {
@ -118,14 +120,14 @@ describe('config: suggestionsBrowserKey', function () {
expect(await input1.getTagName()).to.equal('input')
expect(await input1.getAttribute('type')).to.equal('text')
const label1 = await webdriver.findElement(By.css('label[for="testInput"]'))
const label1 = await webdriver.findElement(By.css(`label[for="${argumentFieldId('testInput')}"]`))
expect(await label1.getText()).to.contain('Test Input')
const input2 = await getTestInput2()
expect(await input2.getTagName()).to.equal('input')
expect(await input2.getAttribute('type')).to.equal('text')
const label2 = await webdriver.findElement(By.css('label[for="testInput2"]'))
const label2 = await webdriver.findElement(By.css(`label[for="${argumentFieldId('testInput2')}"]`))
expect(await label2.getText()).to.contain('Test Input 2')
})
@ -161,7 +163,7 @@ describe('config: suggestionsBrowserKey', function () {
await openArgumentForm()
const datalist = await webdriver.findElement(By.id('testInput-choices'))
const datalist = await webdriver.findElement(By.id(argumentFieldChoicesId('testInput')))
expect(datalist).to.not.be.null
const options = await getDatalistOptions()
@ -241,7 +243,7 @@ describe('config: suggestionsBrowserKey', function () {
await openArgumentForm()
const datalist1 = await webdriver.findElement(By.id('testInput-choices'))
const datalist1 = await webdriver.findElement(By.id(argumentFieldChoicesId('testInput')))
expect(datalist1).to.not.be.null
const options1 = await getDatalistOptions('testInput')
let foundInInput1 = false
@ -254,7 +256,7 @@ describe('config: suggestionsBrowserKey', function () {
}
expect(foundInInput1).to.be.true
const datalist2 = await webdriver.findElement(By.id('testInput2-choices'))
const datalist2 = await webdriver.findElement(By.id(argumentFieldChoicesId('testInput2')))
expect(datalist2).to.not.be.null
const options2 = await getDatalistOptions('testInput2')
let foundInInput2 = false