fix: prefix form arguments to prevent collisions with other page elem… (#1072)
This commit is contained in:
commit
ee97906808
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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...'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue