Files
threshold-ui/ui/admin/tests/integration/components/form/field/key-value-test.js
T

397 lines
13 KiB
JavaScript

/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn, findAll } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | form/field/key-value', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
// Helper functions
const getKeyInputs = () => findAll('[data-test-key-input]');
const getValueInputs = () => findAll('[data-test-value-input]');
const getDeleteButtons = () => findAll('[data-test-delete-button]');
test('it renders with default empty row', async function (assert) {
this.set('data', [{ key: '', value: '' }]);
await render(hbs`
<Form::Field::KeyValue @data={{this.data}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.dom('[data-test-key-input]').exists();
assert.dom('[data-test-value-input]').exists();
assert.dom('[data-test-add-button]').exists();
assert.dom('[data-test-delete-button]').doesNotExist();
});
test('it renders with provided data', async function (assert) {
this.set('data', [
{ key: 'env', value: 'production' },
{ key: 'region', value: 'us-east-1' },
]);
await render(hbs`
<Form::Field::KeyValue
@data={{this.data}}
@legend="Test Legend"
@helperText="Test helper text">
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
const keyInputs = getKeyInputs();
const valueInputs = getValueInputs();
assert.strictEqual(keyInputs.length, 2);
assert.strictEqual(valueInputs.length, 2);
assert.dom(keyInputs[0]).hasValue('env');
assert.dom(keyInputs[1]).hasValue('region');
assert.dom(valueInputs[0]).hasValue('production');
assert.dom(valueInputs[1]).hasValue('us-east-1');
assert.dom('legend').exists().includesText('Test Legend');
assert.dom('.hds-form-helper-text ').hasText('Test helper text');
});
test('it adds new rows', async function (assert) {
this.set('data', [{ key: '', value: '' }]);
this.set('onAdd', () => {
this.data.push({ key: '', value: '' });
this.set('data', [...this.data]);
});
this.set('onUpdate', (rowData, property, { target: { value } }) => {
rowData[property] = value;
});
await render(hbs`
<Form::Field::KeyValue @data={{this.data}} @onAdd={{this.onAdd}} @onUpdate={{this.onUpdate}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} {{on 'input' (fn R.updateRow 'key')}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} {{on 'input' (fn R.updateRow 'value')}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.strictEqual(getKeyInputs().length, 1, 'Initially has one row');
await click('[data-test-add-button]');
assert.strictEqual(
getKeyInputs().length,
2,
'After clicking add, has two rows',
);
assert.strictEqual(getValueInputs().length, 2);
// Fill in the second row
await fillIn(getKeyInputs()[1], 'test-key');
await fillIn(getValueInputs()[1], 'test-value');
// Check that the data was updated
assert.strictEqual(this.data.length, 2);
assert.strictEqual(this.data[1].key, 'test-key');
assert.strictEqual(this.data[1].value, 'test-value');
});
test('it removes rows', async function (assert) {
this.set('data', [
{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2' },
]);
this.set('onRemove', (rowData) => {
// Remove the row from the data array
let newData = this.data.filter((item) => item !== rowData);
// Ensure at least one empty row remains
if (newData.length === 0) {
newData = [{ key: '', value: '' }];
}
this.set('data', newData);
});
await render(hbs`
<Form::Field::KeyValue @data={{this.data}} @onRemove={{this.onRemove}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} {{on 'input' (fn R.updateRow 'key')}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} {{on 'input' (fn R.updateRow 'value')}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.strictEqual(getKeyInputs().length, 2, 'Initially has two rows');
// Remove first row
await click(getDeleteButtons()[0]);
assert.strictEqual(
getKeyInputs().length,
1,
'After removing first row, has one row',
);
assert.dom('[data-test-key-input]').hasValue('key2');
assert.dom('[data-test-value-input]').hasValue('value2');
// Remove second row - should ensure at least one empty row remains
await click(getDeleteButtons()[0]);
assert.strictEqual(
getKeyInputs().length,
1,
'After removing all rows, still has one empty row',
);
assert
.dom('[data-test-key-input]')
.hasValue('', 'The remaining row has empty key');
assert
.dom('[data-test-value-input]')
.hasValue('', 'The remaining row has empty value');
});
test('it handles different field types', async function (assert) {
this.set('data', [{ key: 'k', value: 'v', id: 1 }]);
// Test TextInput and Select fields
await render(hbs`
<Form::Field::KeyValue @data={{this.data}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput name="test-key" data-test-key-input @value={{R.rowData.key}} />
</R.Field>
<R.Field as |F|>
<F.Select name="test-value" data-test-value-input>
<option value={{R.rowData.value}} selected />
</F.Select>
</R.Field>
<R.Field as |F|>
<F.TextInput name="test-id" data-test-id-input @value={{R.rowData.id}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.dom('[data-test-key-input]').hasValue('k');
assert.dom('[data-test-value-input]').hasValue('v');
assert.dom('[data-test-id-input]').hasValue('1');
// Test Textarea field
await render(hbs`
<Form::Field::KeyValue @data={{this.data}}>
<:row as |R|>
<R.Field as |F|>
<F.Textarea name="test-key" data-test-key-input @value={{R.rowData.key}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.dom('[data-test-key-input]').hasValue('k');
// Test single field
this.set('data', [{ key: '' }]);
await render(hbs`
<Form::Field::KeyValue @data={{this.data}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.dom('[data-test-key-input]').exists();
assert.dom('[data-test-value-input]').doesNotExist();
assert.dom('[data-test-add-button]').exists();
assert.dom('[data-test-delete-button]').doesNotExist();
});
test('it displays error messages in footer', async function (assert) {
this.set('data', [{ key: '', value: '' }]);
this.set('errors', [
{ message: 'Key is required' },
{ message: 'Value must be unique' },
]);
await render(hbs`
<Form::Field::KeyValue @data={{this.data}} @errors={{this.errors}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.dom('[data-test-error-message]').exists().hasText('Key is required');
assert
.dom('[data-test-error-message]:nth-child(2)')
.hasText('Value must be unique');
});
test('it does not show error messages when no errors are provided', async function (assert) {
this.set('data', [{ key: '', value: '' }]);
await render(hbs`
<Form::Field::KeyValue @data={{this.data}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.dom('[data-test-error-message]').doesNotExist();
});
test('it updates both key and value fields', async function (assert) {
this.set('data', [{ key: '', value: '' }]);
this.set('onUpdate', (rowData, property, { target: { value } }) => {
rowData[property] = value;
});
await render(hbs`
<Form::Field::KeyValue @data={{this.data}} @onUpdate={{this.onUpdate}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} {{on 'input' (fn R.updateRow 'key')}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} {{on 'input' (fn R.updateRow 'value')}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
await fillIn('[data-test-key-input]', 'test-key');
await fillIn('[data-test-value-input]', 'test-value');
assert.strictEqual(this.data.length, 1);
assert.strictEqual(this.data[0].key, 'test-key');
assert.strictEqual(this.data[0].value, 'test-value');
});
test('delete button shows per row based on index and data', async function (assert) {
this.set('data', [
{ key: '', value: '' },
{ key: '', value: '' },
{ key: 'key3', value: 'value3' },
]);
await render(hbs`
<Form::Field::KeyValue @data={{this.data}}>
<:row as |R|>
<R.Field as |F|>
<F.TextInput data-test-key-input @value={{R.rowData.key}} />
</R.Field>
<R.Field as |F|>
<F.TextInput data-test-value-input @value={{R.rowData.value}} />
</R.Field>
{{#if R.canDelete}}
<R.DeleteRowButton data-test-delete-button @onClick={{R.removeRow}} />
{{/if}}
</:row>
<:footer as |F|>
<F.AddRowButton />
</:footer>
</Form::Field::KeyValue>
`);
assert.strictEqual(getDeleteButtons().length, 3);
});
});