[VAULT-34734] UI: Move the text input block in FormField under the HDS block (#30859)

* [UI] Updated `FormField` to use `Hds::Form::TextInput` for `type=string` and `type=number`  (#34733)

* [UI] Updated `FormField` tests for `type=string` and `type=number` (#34733)

* [UI] Fixed failing test cases from `FormField` `type=string` and `type=number` updates (#34733)

* [UI] Removed checks for this.hideLabel and this.labelString (#34733)

* [UI] Standardize test cases for `FormField` `editType=undefined` (#34733)

* [UI] Update `editType=undefined` tests to use `assert.true` (#34733)

* [UI] Update logic of isHdsFormField to align to previous template (#34733)

* Update ui/tests/integration/components/form-field-test.js

Co-authored-by: Cristiano Rastelli <public@didoo.net>

* [UI] Added `CharacterCount` to `FormField` text input (#34733)

---------

Co-authored-by: Cristiano Rastelli <public@didoo.net>
This commit is contained in:
Dylan Hyun 2025-07-11 14:59:45 -04:00 committed by GitHub
parent 2f4167ee9a
commit 046c33bc72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 270 additions and 146 deletions

View File

@ -219,6 +219,44 @@
<F.Error data-test-validation-error={{this.valuePath}}>{{this.validationError}}</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{else}}
<Hds::Form::TextInput::Field
name={{@attr.name}}
@id={{@attr.name}}
@value={{get @model this.valuePath}}
@isInvalid={{this.validationError}}
readonly={{this.isReadOnly}}
disabled={{and @attr.options.editDisabled (not @model.isNew)}}
autocomplete="off"
spellcheck="false"
placeholder={{@attr.options.placeholder}}
maxLength={{@attr.options.characterLimit}}
{{on "change" this.onChangeWithEvent}}
{{on "input" this.onChangeWithEvent}}
{{on "keyup" this.handleKeyUp}}
data-test-input={{@attr.name}}
as |F|
>
<F.Label data-test-form-field-label>{{this.labelString}}</F.Label>
{{#if this.helpTextString}}
<F.HelperText data-test-help-text={{@attr.options.helpText}}>{{this.helpTextString}}</F.HelperText>
{{/if}}
{{#if @attr.options.subText}}
<F.HelperText data-test-help-text={{@attr.options.subText}}>
{{@attr.options.subText}}
{{#if @attr.options.docLink}}
<DocLink @path={{@attr.options.docLink}} data-test-doc-link={{@attr.options.docLink}}>See our documentation</DocLink>
for help.
{{/if}}
</F.HelperText>
{{/if}}
{{#if @attr.options.characterLimit}}
<F.CharacterCount @maxLength={{@attr.options.characterLimit}} />
{{/if}}
{{#if this.validationError}}
<F.Error data-test-validation-error={{this.valuePath}}>{{this.validationError}}</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{/if}}
{{else if (or (eq @attr.type "boolean") (eq @attr.options.editType "boolean"))}}
<Hds::Form::Checkbox::Field
@ -475,23 +513,6 @@
{{/if}}
</p>
{{/if}}
{{else}}
{{! Regular Text Input }}
<input
data-test-input={{@attr.name}}
id={{@attr.name}}
readonly={{this.isReadOnly}}
disabled={{and @attr.options.editDisabled (not @model.isNew)}}
autocomplete="off"
spellcheck="false"
placeholder={{@attr.options.placeholder}}
value={{get @model this.valuePath}}
{{on "change" this.onChangeWithEvent}}
{{on "input" this.onChangeWithEvent}}
{{on "keyup" this.handleKeyUp}}
class="input {{if this.validationError 'has-error-border'}}"
maxLength={{@attr.options.characterLimit}}
/>
{{/if}}
</div>
{{else if (eq @attr.type "object")}}

View File

@ -120,8 +120,10 @@ export default class FormFieldComponent extends Component {
} else if (type === 'number' || type === 'string') {
if (options?.editType === 'textarea' || options?.editType === 'password') {
return true;
} else {
} else if (options?.editType === 'json') {
return false;
} else {
return true;
}
} else if (type === 'boolean' || options?.editType === 'boolean') {
return true;

View File

@ -245,7 +245,7 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
assert
.dom(CUSTOM_MESSAGES.modal('preview image'))
.doesNotExist('preview image does not show because you have a missing title');
assert.dom(CUSTOM_MESSAGES.input('title')).hasClass('has-error-border', 'error around title shows');
assert.dom(GENERAL.validationErrorByAttr('title')).exists();
});
// unauthenticated messages
@ -340,7 +340,7 @@ module('Acceptance | Enterprise | config-ui/message', function (hooks) {
await fillIn('[data-test-kv-value="0"]', 'www.learn.com');
await click(GENERAL.button('preview'));
assert.dom(CUSTOM_MESSAGES.modal('preview image')).doesNotExist('preview image does not show');
assert.dom(CUSTOM_MESSAGES.input('title')).hasClass('has-error-border', 'error around title shows');
assert.dom(GENERAL.validationErrorByAttr('title')).exists();
});
test('cleanup message pollution', async function (assert) {

View File

@ -193,7 +193,7 @@ module('Acceptance | mfa-method', function (hooks) {
await click('[data-test-mleh-radio="skip"]');
await click('[data-test-mfa-create-save]');
assert
.dom('[data-test-inline-error-message]')
.dom('[data-test-validation-error]')
.exists({ count: required.length }, `Required field validations display for ${type}`);
for (const field of required) {

View File

@ -84,7 +84,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret form -- validations
await click(FORM.saveBtn);
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't be blank.");
await typeIn(FORM.inputByAttr('path'), secretPath);
assert
.dom(FORM.validationWarning)
@ -167,9 +167,9 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret
await typeIn(FORM.inputByAttr('path'), 'my/');
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't end in forward slash '/'.");
await typeIn(FORM.inputByAttr('path'), 'secret');
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
assert.dom(GENERAL.validationErrorByAttr('path')).doesNotExist('form validation goes away');
await fillIn(FORM.keyInput(), 'password');
await fillIn(FORM.maskedValueInput(), 'kittens1234');
@ -182,10 +182,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// MaxVersions validation
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('Maximum versions must be a number.');
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('You cannot go over 16 characters.');
await fillIn(FORM.inputByAttr('maxVersions'), '7');
// Fill in other metadata
@ -378,7 +378,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret form -- validations
await click(FORM.saveBtn);
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't be blank.");
await typeIn(FORM.inputByAttr('path'), secretPath);
assert
.dom(FORM.validationWarning)
@ -425,9 +425,9 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret
await typeIn(FORM.inputByAttr('path'), 'my/');
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't end in forward slash '/'.");
await typeIn(FORM.inputByAttr('path'), 'secret');
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
assert.dom(GENERAL.validationErrorByAttr('path')).doesNotExist('form validation goes away');
await fillIn(FORM.keyInput(), 'password');
await fillIn(FORM.maskedValueInput(), 'kittens1234');
@ -440,10 +440,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// MaxVersions validation
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('Maximum versions must be a number.');
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('You cannot go over 16 characters.');
await fillIn(FORM.inputByAttr('maxVersions'), '7');
// Fill in other metadata
@ -527,7 +527,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret form -- validations
await click(FORM.saveBtn);
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't be blank.");
await typeIn(FORM.inputByAttr('path'), secretPath);
assert
.dom(FORM.validationWarning)
@ -574,9 +574,9 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret
await typeIn(FORM.inputByAttr('path'), 'my/');
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't end in forward slash '/'.");
await typeIn(FORM.inputByAttr('path'), 'secret');
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
assert.dom(GENERAL.validationErrorByAttr('path')).doesNotExist('form validation goes away');
await fillIn(FORM.keyInput(), 'password');
await fillIn(FORM.maskedValueInput(), 'kittens1234');
@ -589,10 +589,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// MaxVersions validation
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('Maximum versions must be a number.');
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('You cannot go over 16 characters.');
await fillIn(FORM.inputByAttr('maxVersions'), '7');
// Fill in other metadata
@ -678,7 +678,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret form -- validations
await click(FORM.saveBtn);
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't be blank.");
await typeIn(FORM.inputByAttr('path'), secretPath);
assert
.dom(FORM.validationWarning)
@ -743,9 +743,9 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret
await typeIn(FORM.inputByAttr('path'), 'my/');
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't end in forward slash '/'.");
await typeIn(FORM.inputByAttr('path'), 'secret');
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
assert.dom(GENERAL.validationErrorByAttr('path')).doesNotExist('form validation goes away');
await fillIn(FORM.keyInput(), 'password');
await fillIn(FORM.maskedValueInput(), 'kittens1234');
@ -758,10 +758,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// MaxVersions validation
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('Maximum versions must be a number.');
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('You cannot go over 16 characters.');
await fillIn(FORM.inputByAttr('maxVersions'), '7');
// Fill in other metadata
@ -888,7 +888,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret form -- validations
await click(FORM.saveBtn);
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't be blank.");
await typeIn(FORM.inputByAttr('path'), secretPath);
assert
.dom(FORM.validationWarning)
@ -971,9 +971,9 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// Create secret
await typeIn(FORM.inputByAttr('path'), 'my/');
assert.dom(FORM.validation('path')).hasText("Path can't end in forward slash '/'.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't end in forward slash '/'.");
await typeIn(FORM.inputByAttr('path'), 'secret');
assert.dom(FORM.validation('path')).doesNotExist('form validation goes away');
assert.dom(GENERAL.validationErrorByAttr('path')).doesNotExist('form validation goes away');
await fillIn(FORM.keyInput(), 'password');
await fillIn(FORM.maskedValueInput(), 'kittens1234');
@ -986,10 +986,10 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook
// MaxVersions validation
await fillIn(FORM.inputByAttr('maxVersions'), 'seven');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('Maximum versions must be a number.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('Maximum versions must be a number.');
await fillIn(FORM.inputByAttr('maxVersions'), '99999999999999999');
await click(FORM.saveBtn);
assert.dom(FORM.validation('maxVersions')).hasText('You cannot go over 16 characters.');
assert.dom(GENERAL.validationErrorByAttr('maxVersions')).hasText('You cannot go over 16 characters.');
await fillIn(FORM.inputByAttr('maxVersions'), '7');
// Fill in other metadata
@ -1104,7 +1104,7 @@ path "${this.backend}/metadata/*" {
// Create secret form -- validations
await click(FORM.saveBtn);
assert.dom(FORM.invalidFormAlert).hasText('There is an error with this form.');
assert.dom(FORM.validation('path')).hasText("Path can't be blank.");
assert.dom(GENERAL.validationErrorByAttr('path')).hasText("Path can't be blank.");
await typeIn(FORM.inputByAttr('path'), secretPath);
assert.dom(PAGE.create.metadataSection).doesNotExist('Hides metadata section by default');

View File

@ -117,6 +117,7 @@ export const FORM = {
inlineAlert: '[data-test-inline-alert]',
validation: (attr) => `[data-test-field="${attr}"] [data-test-inline-alert]`,
messageError: '[data-test-message-error]',
validationError: (attr) => `[data-test-validation-error="${attr}"]`,
validationWarning: '[data-test-validation-warning]',
invalidFormAlert: '[data-test-invalid-form-alert]',
versionAlert: '[data-test-secret-version-alert]',

View File

@ -78,7 +78,7 @@ module('Integration | Component | client count config', function (hooks) {
await fillIn('[data-test-input="retentionMonths"]', 20);
await click(GENERAL.submitButton);
assert
.dom('[data-test-inline-error-message]')
.dom(GENERAL.validationErrorByAttr('retentionMonths'))
.hasText(
'Retention period must be greater than or equal to 48.',
'Validation error shows for min retention period'
@ -86,7 +86,7 @@ module('Integration | Component | client count config', function (hooks) {
await fillIn('[data-test-input="retentionMonths"]', 90);
await click(GENERAL.submitButton);
assert
.dom('[data-test-inline-error-message]')
.dom(GENERAL.validationErrorByAttr('retentionMonths'))
.hasText(
'Retention period must be less than or equal to 60.',
'Validation error shows for max retention period'
@ -142,7 +142,7 @@ module('Integration | Component | client count config', function (hooks) {
await fillIn('[data-test-input="retentionMonths"]', 5);
await click(GENERAL.submitButton);
assert
.dom('[data-test-inline-error-message]')
.dom(GENERAL.validationErrorByAttr('retentionMonths'))
.hasText(
'Retention period must be greater than or equal to 24.',
'Validation error shows for incorrect retention period'

View File

@ -125,15 +125,12 @@ module('Integration | Component | messages/page/create-and-edit', function (hook
});
test('it should have form vaildations', async function (assert) {
assert.expect(2);
await this.renderComponent();
await click(GENERAL.submitButton);
assert
.dom(CUSTOM_MESSAGES.input('title'))
.hasClass('has-error-border', 'show error border for title field');
assert
.dom(`${CUSTOM_MESSAGES.fieldValidation('title')} ${CUSTOM_MESSAGES.inlineErrorMessage}`)
.hasText('Title is required.');
assert.dom(`${GENERAL.validationErrorByAttr('title')}`).hasText('Title is required.');
assert.dom(`${GENERAL.validationErrorByAttr('message')}`).hasText('Message is required.');
});

View File

@ -107,28 +107,6 @@ module('Integration | Component | form field', function (hooks) {
// LEGACY FORM FIELDS
// ------------------
test('it renders: string', async function (assert) {
const [model, spy] = await setup.call(this, createAttr('foo', 'string', { defaultValue: 'default' }));
assert.strictEqual(component.fields.objectAt(0).labelValue, 'Foo', 'renders a label');
assert.strictEqual(component.fields.objectAt(0).inputValue, 'default', 'renders default value');
assert.ok(component.hasInput, 'renders input for string');
await component.fields.objectAt(0).input('bar').change();
assert.strictEqual(model.get('foo'), 'bar');
assert.ok(spy.calledWith('foo', 'bar'), 'onChange called with correct args');
});
test('it renders: number', async function (assert) {
const [model, spy] = await setup.call(this, createAttr('foo', 'number', { defaultValue: 5 }));
assert.strictEqual(component.fields.objectAt(0).labelValue, 'Foo', 'renders a label');
assert.strictEqual(component.fields.objectAt(0).inputValue, '5', 'renders default value');
assert.ok(component.hasInput, 'renders input for number');
await component.fields.objectAt(0).input(8).change();
assert.strictEqual(model.get('foo'), '8');
assert.ok(spy.calledWith('foo', '8'), 'onChange called with correct args');
});
test('it renders: object', async function (assert) {
await setup.call(this, createAttr('foo', 'object'));
assert.dom('[data-test-component="json-editor-title"]').hasText('Foo', 'renders a label');
@ -283,14 +261,13 @@ module('Integration | Component | form field', function (hooks) {
assert.strictEqual(component.fields.objectAt(0).labelValue, 'Not Foo', 'renders the label from options');
});
test('it renders a help tooltip and placeholder', async function (assert) {
test('it renders a help tooltip', async function (assert) {
await setup.call(
this,
createAttr('foo', 'string', { helpText: 'Here is some help text', placeholder: 'example::value' })
createAttr('foo', 'string', { editType: 'stringArray', helpText: 'Here is some help text' })
);
await component.tooltipTrigger();
assert.ok(component.hasTooltip, 'renders the tooltip component');
assert.dom(GENERAL.inputByAttr('foo')).hasAttribute('placeholder', 'example::value');
});
test('it should not expand and toggle ttl when default 0s value is present', async function (assert) {
@ -1207,4 +1184,131 @@ module('Integration | Component | form field', function (hooks) {
.exists('Validation warning renders')
.hasText('Warning message #1 Warning message #2', 'Validation warnings are combined');
});
// editType === undefined && (type === 'string' || type === 'number')
test('it renders: editType=undefined type=string - as Hds::Form::TextInput', async function (assert) {
const [model, spy] = await setup.call(this, createAttr('myfield', 'string', { defaultValue: 'default' }));
assert
.dom('.field [class^="hds-form-field"] input[type="text"].hds-form-text-input')
.exists('renders as Hds::Form::TextInput::Field');
assert
.dom(`input[type=text]`)
.exists('renders input[type="text"]')
.hasAttribute(
'data-test-input',
'myfield',
'input[type="text"] has correct `data-test-input` attribute'
)
.hasAttribute('name', 'myfield', 'input[type="text"] has correct `name` attribute')
.hasAttribute('id', 'myfield', 'input[type="text"] has correct `id` attribute');
assert.dom(GENERAL.fieldLabel()).hasText('Myfield', 'renders the input[type="text"] label');
assert.dom(GENERAL.inputByAttr('myfield')).hasValue('default', 'renders default value');
await fillIn(GENERAL.inputByAttr('myfield'), 'bar');
assert.strictEqual(model.get('myfield'), 'bar');
assert.true(spy.calledWith('myfield', 'bar'), 'onChange called with correct args');
});
test('it renders: editType=undefined type=number - as Hds::Form::TextInput', async function (assert) {
const [model, spy] = await setup.call(this, createAttr('myfield', 'number', { defaultValue: 123 }));
assert
.dom('.field [class^="hds-form-field"] input[type="text"].hds-form-text-input')
.exists('renders as Hds::Form::TextInput::Field');
assert
.dom(`input[type=text]`)
.exists('renders input[type="text"]')
.hasAttribute(
'data-test-input',
'myfield',
'input[type="text"] has correct `data-test-input` attribute'
)
.hasAttribute('name', 'myfield', 'input[type="text"] has correct `name` attribute')
.hasAttribute('id', 'myfield', 'input[type="text"] has correct `id` attribute');
assert.dom(GENERAL.fieldLabel()).hasText('Myfield', 'renders the input[type="text"] label');
assert.dom(GENERAL.inputByAttr('myfield')).hasValue('123', 'renders default value');
await fillIn(GENERAL.inputByAttr('myfield'), 1234);
assert.strictEqual(model.get('myfield'), '1234');
assert.true(spy.calledWith('myfield', '1234'), 'onChange called with correct args');
});
test('it renders: editType=undefined - with passed characterLimit, docLink, editDisabled, helpText, label, placeholder, subText', async function (assert) {
await setup.call(
this,
createAttr('myfield', 'string', {
characterLimit: 10,
docLink: '/docs',
editDisabled: true,
helpText: 'Some helpText',
label: 'Custom label',
placeholder: 'Custom placeholder',
subText: 'Some subText',
})
);
assert.dom(GENERAL.fieldLabel()).hasText('Custom label', 'renders the custom label from options');
assert
.dom('.hds-form-field__character-count')
.containsText('10', 'renders the characterLimit helper text from options');
assert
.dom(GENERAL.inputByAttr('myfield'))
.hasAttribute('placeholder', 'Custom placeholder', 'renders the placeholder from options')
.hasAttribute('disabled', '', 'renders the disabled attribute from options')
.hasAttribute('maxlength', '10', 'renders the characterLimit from options');
assert
.dom(GENERAL.helpTextByAttr('Some subText'))
.exists('renders `subText` option as HelperText')
.hasText(
'Some subText See our documentation for help.',
'renders the right subText string from options'
);
assert
.dom(`${GENERAL.helpTextByAttr('Some subText')} ${GENERAL.docLinkByAttr('/docs')}`)
.exists('renders `docLink` option as as link inside the subText');
assert
.dom(GENERAL.helpTextByAttr('Some helpText'))
.exists('renders `helpText` option as HelperText')
.hasText('Some helpText', 'renders the right helpText string from options');
});
test('it renders: editType=undefined - with readOnly when mode=edit', async function (assert) {
this.setProperties({
attr: createAttr('myfield', 'string', { readOnly: true }),
mode: 'edit',
model: { myfield: false },
onChange: () => {},
});
await render(
hbs`<FormField @attr={{this.attr}} @model={{this.model}} @mode={{this.mode}} @onChange={{this.onChange}} />`
);
assert
.dom(GENERAL.inputByAttr('myfield'))
.hasAttribute('readonly', '', 'renders the readOnly attribute from options');
});
test('it renders: editType=undefined - with validation errors and warnings', async function (assert) {
this.setProperties({
attr: createAttr('myfield', 'string'),
model: { myfield: false },
modelValidations: {
myfield: {
isValid: false,
errors: ['Error message #1', 'Error message #2'],
warnings: ['Warning message #1', 'Warning message #2'],
},
},
onChange: () => {},
});
await render(
hbs`<FormField @attr={{this.attr}} @model={{this.model}} @modelValidations={{this.modelValidations}} @onChange={{this.onChange}} />`
);
assert
.dom(GENERAL.validationErrorByAttr('myfield'))
.exists('Validation error renders')
.hasText('Error message #1 Error message #2', 'Validation errors are combined');
assert
.dom(GENERAL.validationWarningByAttr('myfield'))
.exists('Validation warning renders')
.hasText('Warning message #1 Warning message #2', 'Validation warnings are combined');
});
});

View File

@ -199,7 +199,7 @@ module('Integration | Component | keymgmt/provider-edit', function (hooks) {
assert.dom(`[${ts}-creds-title]`).doesNotExist('New credentials header hidden in create mode');
await click(`[${ts}-submit]`);
assert.dom('[data-test-inline-error-message]').exists('Validation error messages shown');
assert.dom('[data-test-validation-error]').exists('Validation error messages shown');
await fillIn('[data-test-input="provider"]', 'azurekeyvault');
['client_id', 'client_secret', 'tenant_id'].forEach((prop) => {

View File

@ -231,7 +231,7 @@ module('Integration | Component | kubernetes | Page::Configure', function (hooks
await click('[data-test-config-save]');
assert
.dom(`${GENERAL.validationErrorByAttr('kubernetesHost')} [data-test-inline-error-message]`)
.dom(GENERAL.validationErrorByAttr('kubernetesHost'))
.hasText('Kubernetes host is required', 'Error renders for required field');
assert.dom('[data-test-alert]').hasText('There is an error with this form.', 'Alert renders');
});

View File

@ -163,7 +163,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct
);
await click('[data-test-radio-card="basic"]');
await click('[data-test-submit]');
assert.dom('[data-test-inline-error-message]').hasText('Name is required', 'Validation error renders');
assert.dom(GENERAL.validationErrorByAttr('name')).hasText('Name is required', 'Validation error renders');
await fillIn('[data-test-input="name"]', 'role-1');
await fillIn('[data-test-input="serviceAccountName"]', 'default');
await click('[data-test-submit]');
@ -327,10 +327,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct
);
await click('[data-test-radio-card="basic"]');
await click('[data-test-submit]');
assert
.dom('[data-test-input="name"]')
.hasClass('has-error-border', 'shows border error on input with error');
assert.dom('[data-test-inline-error-message]').hasText('Name is required');
assert.dom(GENERAL.validationErrorByAttr('name')).hasText('Name is required');
assert
.dom('[data-test-invalid-form-alert] [data-test-inline-error-message]')
.hasText('There is an error with this form.');

View File

@ -148,7 +148,7 @@ module('Integration | Component | kv | Page::Secret::Metadata::Edit', function (
await fillIn(FORM.inputByAttr('maxVersions'), 'a');
await click(FORM.saveBtn);
assert
.dom(FORM.inlineAlert)
.dom(FORM.validationError('maxVersions'))
.hasText('Maximum versions must be a number.', 'Validation message is shown for max_versions');
await click(FORM.cancelBtn);

View File

@ -9,7 +9,7 @@ import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { Response } from 'miragejs';
import { hbs } from 'ember-cli-htmlbars';
import { click, fillIn, findAll, render, typeIn } from '@ember/test-helpers';
import { click, fillIn, render, typeIn } from '@ember/test-helpers';
import codemirror from 'vault/tests/helpers/codemirror';
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
import sinon from 'sinon';
@ -242,11 +242,11 @@ module('Integration | Component | kv-v2 | Page::Secrets::Create', function (hook
await fillIn(FORM.inputByAttr('path'), ''); // clear input
await typeIn(FORM.inputByAttr('path'), 'slash/');
assert.dom(FORM.validation('path')).hasText(`Path can't end in forward slash '/'.`);
assert.dom(FORM.validationError('path')).hasText(`Path can't end in forward slash '/'.`);
await typeIn(FORM.inputByAttr('path'), 'secret');
assert
.dom(FORM.validation('path'))
.dom(FORM.validationError('path'))
.doesNotExist('it removes validation on key up when secret contains slash but does not end in one');
await click(GENERAL.toggleInput('json'));
@ -258,9 +258,8 @@ module('Integration | Component | kv-v2 | Page::Secrets::Create', function (hook
codemirror().setValue('{}'); // clear linting error
await fillIn(FORM.inputByAttr('path'), '');
await click(FORM.saveBtn);
const [pathValidation, formAlert] = findAll(FORM.inlineAlert);
assert.dom(pathValidation).hasText(`Path can't be blank.`);
assert.dom(formAlert).hasText('There is an error with this form.');
assert.dom(FORM.validationError('path')).hasText(`Path can't be blank.`);
assert.dom(FORM.inlineAlert).hasText('There is an error with this form.');
});
test('it toggles JSON view and saves modified data', async function (assert) {

View File

@ -81,10 +81,10 @@ module('Integration | Component | ldap | Page::Configure', function (hooks) {
await click(selectors.save);
assert
.dom('[data-test-field="binddn"] [data-test-inline-error-message]')
.dom(GENERAL.validationErrorByAttr('binddn'))
.hasText('Administrator distinguished name is required.', 'Validation message renders for binddn');
assert
.dom('[data-test-field="bindpass"] [data-test-inline-error-message]')
.dom(GENERAL.validationErrorByAttr('bindpass'))
.hasText('Administrator password is required.', 'Validation message renders for bindpass');
assert
.dom('[data-test-invalid-form-message]')

View File

@ -11,6 +11,7 @@ import { render, click, fillIn } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { ldapRoleID } from 'vault/adapters/ldap/role';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (hooks) {
setupRenderingTest(hooks);
@ -135,9 +136,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h
await click('[data-test-submit]');
fields.forEach((field) => {
assert
.dom(`[data-test-field="${field}"] [data-test-inline-error-message]`)
.exists('Validation message renders');
assert.dom(GENERAL.validationErrorByAttr(field)).exists('Validation message renders');
});
assert

View File

@ -102,10 +102,12 @@ module('Integration | Component | oidc/client-form', function (hooks) {
const validationErrors = findAll(SELECTORS.inlineAlert);
assert
.dom(validationErrors[0])
.dom(GENERAL.validationErrorByAttr('name'))
.hasText('Name is required. Name cannot contain whitespace.', 'Validation messages are shown for name');
assert.dom(validationErrors[1]).hasText('Key is required.', 'Validation message is shown for key');
assert.dom(validationErrors[2]).hasText('There are 3 errors with this form.', 'Renders form error count');
assert
.dom(GENERAL.validationErrorByAttr('key'))
.hasText('Key is required.', 'Validation message is shown for key');
assert.dom(validationErrors[1]).hasText('There are 3 errors with this form.', 'Renders form error count');
// fill out form with valid inputs
await clickTrigger();

View File

@ -12,6 +12,7 @@ import oidcConfigHandlers from 'vault/mirage/handlers/oidc-config';
import { OIDC_BASE_URL, CLIENT_LIST_RESPONSE, SELECTORS } from 'vault/tests/helpers/oidc-config';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { capabilitiesStub, overrideResponse } from 'vault/tests/helpers/stubs';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | oidc/key-form', function (hooks) {
setupRenderingTest(hooks);
@ -57,11 +58,12 @@ module('Integration | Component | oidc/key-form', function (hooks) {
await fillIn('[data-test-input="name"]', ' ');
await click(SELECTORS.keySaveButton);
const validationErrors = findAll(SELECTORS.inlineAlert);
assert
.dom(validationErrors[0])
.dom(GENERAL.validationErrorByAttr('name'))
.hasText('Name is required. Name cannot contain whitespace.', 'Validation messages are shown for name');
assert.dom(validationErrors[1]).hasText('There are 2 errors with this form.', 'Renders form error count');
assert
.dom(SELECTORS.inlineAlert)
.hasText('There are 2 errors with this form.', 'Renders form error count');
assert.dom('[data-test-oidc-radio="limited"] input').isDisabled('limit radio button disabled on create');
await fillIn('[data-test-input="name"]', 'test-key');

View File

@ -13,6 +13,7 @@ import { SELECTORS, OIDC_BASE_URL, CLIENT_LIST_RESPONSE } from 'vault/tests/help
import parseURL from 'core/utils/parse-url';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { capabilitiesStub, overrideResponse } from 'vault/tests/helpers/stubs';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
const ISSUER_URL = 'http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider';
@ -81,11 +82,12 @@ module('Integration | Component | oidc/provider-form', function (hooks) {
await fillIn('[data-test-input="name"]', ' ');
await click(SELECTORS.providerSaveButton);
const validationErrors = findAll(SELECTORS.inlineAlert);
assert
.dom(validationErrors[0])
.dom(GENERAL.validationErrorByAttr('name'))
.hasText('Name is required. Name cannot contain whitespace.', 'Validation messages are shown for name');
assert.dom(validationErrors[1]).hasText('There are 2 errors with this form.', 'Renders form error count');
assert
.dom(SELECTORS.inlineAlert)
.hasText('There are 2 errors with this form.', 'Renders form error count');
await click('[data-test-oidc-radio="limited"]');
assert

View File

@ -5,11 +5,12 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, fillIn, click, findAll } from '@ember/test-helpers';
import { render, fillIn, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { SELECTORS, OIDC_BASE_URL } from 'vault/tests/helpers/oidc-config';
import { capabilitiesStub } from 'vault/tests/helpers/stubs';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | oidc/scope-form', function (hooks) {
setupRenderingTest(hooks);
@ -20,7 +21,7 @@ module('Integration | Component | oidc/scope-form', function (hooks) {
});
test('it should save new scope', async function (assert) {
assert.expect(9);
assert.expect(8);
this.server.post('/identity/oidc/scope/test', (schema, req) => {
assert.ok(true, 'Request made to save scope');
@ -45,13 +46,13 @@ module('Integration | Component | oidc/scope-form', function (hooks) {
// check validation errors
await click(SELECTORS.scopeSaveButton);
const validationErrors = findAll(SELECTORS.inlineAlert);
assert.dom(validationErrors[0]).hasText('Name is required.', 'Validation messages are shown for name');
assert.dom(validationErrors[1]).hasText('There is an error with this form.', 'Renders form error count');
assert
.dom('[data-test-inline-error-message]')
.hasText('Name is required.', 'Validation message is shown for name');
.dom(GENERAL.validationErrorByAttr('name'))
.hasText('Name is required.', 'Validation messages are shown for name');
assert
.dom(SELECTORS.inlineAlert)
.hasText('There is an error with this form.', 'Renders form error count');
// json editor has test coverage so let's just confirm that it renders
assert
.dom('[data-test-input="template"] [data-test-component="json-editor-toolbar"]')

View File

@ -126,12 +126,8 @@ module('Integration | Component | page/pki-issuer-rotate-root', function (hooks)
await fillIn(GENERAL.inputByAttr('issuerName'), 'default');
await click(GENERAL.submitButton);
assert.dom(SELECTORS.validationError).hasText('There are 2 errors with this form.');
assert
.dom(GENERAL.inputByAttr('commonName'))
.hasClass('has-error-border', 'common name has error border');
assert
.dom(GENERAL.inputByAttr('issuerName'))
.hasClass('has-error-border', 'issuer name has error border');
assert.dom(GENERAL.validationErrorByAttr('commonName')).exists();
assert.dom(GENERAL.validationErrorByAttr('issuerName')).exists();
});
test('it sends request to rotate/internal on save when using old root settings', async function (assert) {

View File

@ -105,7 +105,7 @@ module('Integration | Component | pki-generate-csr', function (hooks) {
.dom(GENERAL.validationErrorByAttr('type'))
.hasText('Type is required.', 'Type validation error renders');
assert
.dom('[data-test-field="commonName"] [data-test-inline-alert]')
.dom(GENERAL.validationErrorByAttr('commonName'))
.hasText('Common name is required.', 'Common name validation error renders');
assert.dom('[data-test-alert]').hasText('There are 2 errors with this form.', 'Alert renders');

View File

@ -86,7 +86,7 @@ module('Integration | Component | pki-role-form', function (hooks) {
test('it should save a new pki role with various options selected', async function (assert) {
// Key usage, Key params and Not valid after options are tested in their respective component tests
assert.expect(8);
assert.expect(7);
this.server.post(`/${this.role.backend}/roles/test-role`, (schema, req) => {
assert.ok(true, 'Request made to save role');
const request = JSON.parse(req.requestBody);
@ -123,10 +123,7 @@ module('Integration | Component | pki-role-form', function (hooks) {
await click(GENERAL.submitButton);
assert
.dom(GENERAL.inputByAttr('name'))
.hasClass('has-error-border', 'shows border error on role name field when no role name is submitted');
assert
.dom('[data-test-inline-error-message]')
.dom(GENERAL.validationErrorByAttr('name'))
.includesText('Name is required.', 'show correct error message');
await fillIn(GENERAL.inputByAttr('name'), 'test-role');

View File

@ -65,7 +65,7 @@ module('Integration | Component | SecretEngine/configure-ssh', function (hooks)
await fillIn(GENERAL.inputByAttr('publicKey'), 'hello');
await click(GENERAL.submitButton);
assert
.dom(GENERAL.inlineError)
.dom(GENERAL.validationErrorByAttr('publicKey'))
.hasText(
'You must provide a Public and Private keys or leave both unset.',
'Public key validation error renders.'
@ -74,9 +74,9 @@ module('Integration | Component | SecretEngine/configure-ssh', function (hooks)
await click(GENERAL.inputByAttr('generateSigningKey'));
await click(GENERAL.submitButton);
assert
.dom(GENERAL.inlineError)
.dom(GENERAL.validationErrorByAttr('generateSigningKey'))
.hasText(
'You must provide a Public and Private keys or leave both unset.',
'Provide a Public and Private key or set "Generate Signing Key" to true.',
'Generate signing key validation message shows.'
);
});

View File

@ -5,7 +5,7 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, fillIn, click, findAll } from '@ember/test-helpers';
import { render, fillIn, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
@ -57,21 +57,22 @@ module('Integration | Component | totp/key-form', function (hooks) {
// check validation errors
await click(GENERAL.submitButton);
const validationErrors = findAll(GENERAL.inlineAlert);
assert.dom(validationErrors[0]).hasText(`Name can't be blank.`, 'Validation messages are shown for name');
assert
.dom(validationErrors[1])
.dom(GENERAL.validationErrorByAttr('name'))
.hasText(`Name can't be blank.`, 'Validation messages are shown for name');
assert
.dom(GENERAL.validationErrorByAttr('issuer'))
.hasText(
`Issuer can't be blank when when the key is generated by Vault.`,
'Validation messages are shown for issuer'
);
assert
.dom(validationErrors[2])
.dom(GENERAL.validationErrorByAttr('accountName'))
.hasText(
`Account name can't be blank when the key is generated by Vault.`,
'Validation messages are shown for account name'
);
assert.dom(validationErrors[3]).hasText('There are 3 errors with this form.', 'Renders form error count');
assert.dom(GENERAL.inlineAlert).hasText('There are 3 errors with this form.', 'Renders form error count');
await fillIn('[data-test-input="name"]', 'test-key');
await fillIn('[data-test-input="issuer"]', 'test-issuer');
@ -117,15 +118,16 @@ module('Integration | Component | totp/key-form', function (hooks) {
// check validation errors
await click(GENERAL.submitButton);
const validationErrors = findAll(GENERAL.inlineAlert);
assert.dom(validationErrors[0]).hasText(`Name can't be blank.`, 'Validation messages are shown for name');
assert
.dom(validationErrors[1])
.dom(GENERAL.validationErrorByAttr('name'))
.hasText(`Name can't be blank.`, 'Validation messages are shown for name');
assert
.dom(GENERAL.validationErrorByAttr('key'))
.hasText(
`Key can't be blank if key is being passed from another service and the URL is empty.`,
'Validation messages are shown for issuer'
);
assert.dom(validationErrors[2]).hasText('There are 2 errors with this form.', 'Renders form error count');
assert.dom(GENERAL.inlineAlert).hasText('There are 2 errors with this form.', 'Renders form error count');
await fillIn('[data-test-input="name"]', 'test-key');
await fillIn('[data-test-input="key"]', 'test-root-key');

View File

@ -25,7 +25,9 @@ module('Integration | Helper | transition-to', function (hooks) {
});
test('it does not call transition on render', async function (assert) {
await render(hbs`<button data-test-btn {{on "click" (transition-to "vault.cluster")}}>Click</button>`);
await render(
hbs`<button data-test-btn type="button" {{on "click" (transition-to "vault.cluster")}}>Click</button>`
);
assert.true(this.router.transitionTo.notCalled, 'transitionTo not called on render');
assert.true(this.router.transitionToExternal.notCalled, 'transitionToExternal not called on render');
@ -33,7 +35,7 @@ module('Integration | Helper | transition-to', function (hooks) {
test('it calls transitionTo correctly', async function (assert) {
await render(
hbs`<button data-test-btn {{on "click" (transition-to "vault.cluster" "foobar" "baz")}}>Click</button>`
hbs`<button data-test-btn type="button" {{on "click" (transition-to "vault.cluster" "foobar" "baz")}}>Click</button>`
);
await click('[data-test-btn]');
@ -48,7 +50,7 @@ module('Integration | Helper | transition-to', function (hooks) {
test('it calls transitionToExternal correctly', async function (assert) {
await render(
hbs`<button data-test-btn {{on "click" (transition-to "vault.cluster" "foobar" "baz" external=true)}}>Click</button>`
hbs`<button data-test-btn type="button" {{on "click" (transition-to "vault.cluster" "foobar" "baz" external=true)}}>Click</button>`
);
await click('[data-test-btn]');
@ -68,7 +70,7 @@ module('Integration | Helper | transition-to', function (hooks) {
// This test passing, indirectly means the helper works as expected. Failures might be something like "global failure: TypeError: this.router is undefined"
test('it uses service:app-router when base router undefined', async function (assert) {
await render(
hbs`<button data-test-btn {{on "click" (transition-to "vault.cluster" "foobar" "baz" external=true)}}>Click</button>`,
hbs`<button data-test-btn type="button" {{on "click" (transition-to "vault.cluster" "foobar" "baz" external=true)}}>Click</button>`,
{ owner: this.engine }
);
await click('[data-test-btn]');