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" "stylelint-config-standard": "^40.0.0"
}, },
"scripts": { "scripts": {
"test": "node --test resources/vue/components/*.test.mjs" "test": "node --test resources/vue/components/*.test.mjs resources/vue/utils/*.test.mjs"
}, },
"author": "", "author": "",
"parcelIgnore": [ "parcelIgnore": [

View File

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

View File

@ -31,7 +31,7 @@
> >
<li <li
v-for="(choice, index) in filteredChoices" v-for="(choice, index) in filteredChoices"
:id="`${listboxId}-option-${index}`" :id="listboxOptionId(index)"
:key="choice.value" :key="choice.value"
role="option" role="option"
:aria-selected="choice.value === modelValue" :aria-selected="choice.value === modelValue"
@ -56,6 +56,10 @@ import {
choiceDisplayLabel, choiceDisplayLabel,
syncStateFromModelValue syncStateFromModelValue
} from './choiceComboboxHelpers.js' } from './choiceComboboxHelpers.js'
import {
argumentFieldListboxId,
argumentFieldListboxOptionId
} from '../utils/argumentFieldIds.js'
const props = defineProps({ const props = defineProps({
id: { id: {
@ -91,16 +95,20 @@ const query = ref('')
const isUserFiltering = ref(false) const isUserFiltering = ref(false)
const highlightedIndex = ref(0) const highlightedIndex = ref(0)
const listboxId = computed(() => `${props.id}-listbox`) const listboxId = computed(() => argumentFieldListboxId(props.name))
const activeDescendantId = computed(() => { const activeDescendantId = computed(() => {
if (!isOpen.value || filteredChoices.value.length === 0) { if (!isOpen.value || filteredChoices.value.length === 0) {
return undefined return undefined
} }
return `${listboxId.value}-option-${highlightedIndex.value}` return listboxOptionId(highlightedIndex.value)
}) })
function listboxOptionId (index) {
return argumentFieldListboxOptionId(props.name, index)
}
const placeholderText = computed(() => { const placeholderText = computed(() => {
if (props.required) { if (props.required) {
return 'Search and select...' 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() { export function readPrefilledArgumentsFromNavigation (historyState = globalThis.window?.history?.state) {
const state = window.history.state if (historyState?.prefilledArguments && typeof historyState.prefilledArguments === 'object') {
if (state?.prefilledArguments && typeof state.prefilledArguments === 'object') { return { ...historyState.prefilledArguments }
return { ...state.prefilledArguments }
} }
return {} return {}
} }
export function getInitialArgumentValue(paramName, prefilledArguments = {}) { export function getInitialArgumentValue (paramName, prefilledArguments = {}, search = globalThis.window?.location?.search ?? '') {
const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object' const safePrefilledArguments = prefilledArguments && typeof prefilledArguments === 'object'
? prefilledArguments ? prefilledArguments
: {} : {}
@ -16,6 +15,6 @@ export function getInitialArgumentValue(paramName, prefilledArguments = {}) {
return safePrefilledArguments[paramName] return safePrefilledArguments[paramName]
} }
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(search)
return params.get(paramName) return params.get(paramName)
} }

View File

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

View File

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

View File

@ -5,6 +5,15 @@ import { Condition } from 'selenium-webdriver'
export const DEFAULT_UI_WAIT_MS = 3000 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') const executionDialogStatusBy = By.css('.execution-dialog-status')
export async function getActionButtons () { 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, waitForArgumentFormPage,
waitForLogsPage, waitForLogsPage,
waitForExecutionComplete, waitForExecutionComplete,
argumentFieldId,
} from '../../lib/elements.js' } from '../../lib/elements.js'
async function openCheckboxArgumentForm() { async function openCheckboxArgumentForm() {
@ -21,7 +22,7 @@ async function openCheckboxArgumentForm() {
} }
async function getCheckboxInput() { async function getCheckboxInput() {
return await webdriver.findElement(By.id('confirm')) return await webdriver.findElement(By.id(argumentFieldId('confirm')))
} }
async function submitCheckboxForm() { async function submitCheckboxForm() {
@ -75,7 +76,7 @@ describe('config: checkbox', function () {
expect(await checkboxInput.getTagName()).to.equal('input') expect(await checkboxInput.getTagName()).to.equal('input')
expect(await checkboxInput.getAttribute('type')).to.equal('checkbox') 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') expect(await label.getText()).to.contain('Confirm option')
}) })

View File

@ -7,6 +7,7 @@ import {
takeScreenshotOnFailure, takeScreenshotOnFailure,
waitForArgumentFormReady, waitForArgumentFormReady,
waitForLogsPage, waitForLogsPage,
argumentFieldId,
} from '../../lib/elements.js' } from '../../lib/elements.js'
describe('config: datetime', function () { describe('config: datetime', function () {
@ -32,7 +33,7 @@ describe('config: datetime', function () {
await waitForArgumentFormReady() await waitForArgumentFormReady()
// Find the datetime input field // 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 // Verify it's a datetime-local input type
const inputType = await datetimeInput.getAttribute('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') expect(step).to.equal('1', 'Step attribute should be 1')
// Verify the label is present // 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') expect(await label.getText()).to.contain('Select a date and time')
}) })
@ -57,7 +58,7 @@ describe('config: datetime', function () {
await waitForArgumentFormReady() await waitForArgumentFormReady()
// Find the datetime input field // 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) // Set a datetime value (format: YYYY-MM-DDTHH:mm)
// datetime-local returns values without seconds, backend will add :00 // 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 { By, Condition } from 'selenium-webdriver'
import { import {
DEFAULT_UI_WAIT_MS, DEFAULT_UI_WAIT_MS,
argumentFieldChoicesId,
argumentFieldId,
getRootAndWait, getRootAndWait,
getActionButton, getActionButton,
takeScreenshotOnFailure, takeScreenshotOnFailure,
@ -73,15 +75,15 @@ async function openArgumentForm() {
} }
async function getTestInput() { async function getTestInput() {
return await webdriver.findElement(By.id('testInput')) return await webdriver.findElement(By.id(argumentFieldId('testInput')))
} }
async function getTestInput2() { async function getTestInput2() {
return await webdriver.findElement(By.id('testInput2')) return await webdriver.findElement(By.id(argumentFieldId('testInput2')))
} }
async function getDatalistOptions(inputName = 'testInput') { 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() { async function submitForm() {
@ -118,14 +120,14 @@ describe('config: suggestionsBrowserKey', function () {
expect(await input1.getTagName()).to.equal('input') expect(await input1.getTagName()).to.equal('input')
expect(await input1.getAttribute('type')).to.equal('text') 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') expect(await label1.getText()).to.contain('Test Input')
const input2 = await getTestInput2() const input2 = await getTestInput2()
expect(await input2.getTagName()).to.equal('input') expect(await input2.getTagName()).to.equal('input')
expect(await input2.getAttribute('type')).to.equal('text') 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') expect(await label2.getText()).to.contain('Test Input 2')
}) })
@ -161,7 +163,7 @@ describe('config: suggestionsBrowserKey', function () {
await openArgumentForm() 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 expect(datalist).to.not.be.null
const options = await getDatalistOptions() const options = await getDatalistOptions()
@ -241,7 +243,7 @@ describe('config: suggestionsBrowserKey', function () {
await openArgumentForm() 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 expect(datalist1).to.not.be.null
const options1 = await getDatalistOptions('testInput') const options1 = await getDatalistOptions('testInput')
let foundInInput1 = false let foundInInput1 = false
@ -254,7 +256,7 @@ describe('config: suggestionsBrowserKey', function () {
} }
expect(foundInInput1).to.be.true 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 expect(datalist2).to.not.be.null
const options2 = await getDatalistOptions('testInput2') const options2 = await getDatalistOptions('testInput2')
let foundInInput2 = false let foundInInput2 = false