import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { hbs } from 'ember-cli-htmlbars'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; import { click, typeIn, find, findAll, render } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror'; import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror'; import percySnapshot from '@percy/ember'; import { selectChoose, clickTrigger, } from 'ember-power-select/test-support/helpers'; import faker from 'nomad-ui/mirage/faker'; module('Integration | Component | variable-form', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); setupCodeMirror(hooks); test('passes an accessibility audit', async function (assert) { assert.expect(1); this.set( 'mockedModel', server.create('variable', { keyValues: [{ key: '', value: '' }], }) ); await render(hbs``); await componentA11yAudit(this.element, assert); }); test('shows a single row by default and modifies on "Add More" and "Delete"', async function (assert) { this.set( 'mockedModel', server.create('variable', { keyValues: [{ key: '', value: '' }], }) ); assert.expect(7); await render(hbs``); assert.equal( findAll('div.key-value').length, 1, 'A single KV row exists by default' ); assert .dom('[data-test-add-kv]') .isDisabled( 'The "Add More" button is disabled until key and value are filled' ); await typeIn('.key-value label:nth-child(1) input', 'foo'); assert .dom('[data-test-add-kv]') .isDisabled( 'The "Add More" button is still disabled with only key filled' ); await typeIn('.key-value label:nth-child(2) input', 'bar'); assert .dom('[data-test-add-kv]') .isNotDisabled( 'The "Add More" button is no longer disabled after key and value are filled' ); await click('[data-test-add-kv]'); assert.equal( findAll('div.key-value').length, 2, 'A second KV row exists after adding a new one' ); await typeIn('.key-value:last-of-type label:nth-child(1) input', 'foo'); await typeIn('.key-value:last-of-type label:nth-child(2) input', 'bar'); await click('[data-test-add-kv]'); assert.equal( findAll('div.key-value').length, 3, 'A third KV row exists after adding a new one' ); await click('.key-value button.delete-row'); assert.equal( findAll('div.key-value').length, 2, 'Back down to two rows after hitting delete' ); }); module('editing and creating new key/value pairs', function () { test('it should allow each key/value row to toggle password visibility', async function (assert) { faker.seed(1); this.set( 'mockedModel', server.create('variable', { keyValues: [{ key: 'foo', value: 'bar' }], }) ); assert.expect(6); await render(hbs``); await click('[data-test-add-kv]'); // add a second variable findAll('input.value-input').forEach((input, iter) => { assert.equal( input.getAttribute('type'), 'password', `Value ${iter + 1} is hidden by default` ); }); await click('.key-value button.show-hide-values'); const [firstRow, secondRow] = findAll('input.value-input'); assert.equal( firstRow.getAttribute('type'), 'text', 'Only the row that is clicked on toggles visibility' ); assert.equal( secondRow.getAttribute('type'), 'password', 'Rows that are not clicked remain obscured' ); await click('.key-value button.show-hide-values'); assert.equal( firstRow.getAttribute('type'), 'password', 'Only the row that is clicked on toggles visibility' ); assert.equal( secondRow.getAttribute('type'), 'password', 'Rows that are not clicked remain obscured' ); await percySnapshot(assert); }); }); test('Existing variable shows properties by default', async function (assert) { assert.expect(13); const keyValues = [ { key: 'my-completely-normal-key', value: 'never' }, { key: 'another key, but with spaces', value: 'gonna' }, { key: 'once/more/with/slashes', value: 'give' }, { key: 'and_some_underscores', value: 'you' }, { key: 'and\\now/for-something_completely@different', value: 'up' }, ]; this.set( 'mockedModel', server.create('variable', { path: 'my/path/to', keyValues, }) ); await render(hbs``); assert.equal( findAll('div.key-value').length, 5, 'Shows 5 existing key values' ); assert.equal( findAll('button.delete-row').length, 5, 'Shows "delete" for all five rows' ); assert.equal( findAll('[data-test-add-kv]').length, 1, 'Shows "add more" only on the last row' ); findAll('div.key-value').forEach((row, idx) => { assert.equal( row.querySelector(`label:nth-child(1) input`).value, keyValues[idx].key, `Key ${idx + 1} is correct` ); assert.equal( row.querySelector(`label:nth-child(2) input`).value, keyValues[idx].value, keyValues[idx].value ); }); }); test('Prevent editing path input on existing variables', async function (assert) { assert.expect(3); const variable = await this.server.create('variable', { name: 'foo', namespace: 'bar', path: '/baz/bat', keyValues: [{ key: '', value: '' }], }); variable.isNew = false; this.set('variable', variable); await render(hbs``); assert.dom('input.path-input').hasValue('/baz/bat', 'Path is set'); assert .dom('input.path-input') .isDisabled('Existing variable is in disabled state'); variable.isNew = true; variable.path = ''; this.set('variable', variable); await render(hbs``); assert .dom('input.path-input') .isNotDisabled('New variable is not in disabled state'); }); module('Validation', function () { test('warns when you try to create a path that already exists', async function (assert) { this.server.createList('namespace', 3); this.set( 'mockedModel', server.create('variable', { path: '', keyValues: [{ key: '', value: '' }], }) ); server.create('variable', { path: 'baz/bat', }); server.create('variable', { path: 'baz/bat/qux', namespace: server.db.namespaces[2].id, }); this.set('existingVariables', server.db.variables.toArray()); await render( hbs`` ); await typeIn('.path-input', 'foo/bar'); assert.dom('.duplicate-path-error').doesNotExist(); assert.dom('.path-input').doesNotHaveClass('error'); document.querySelector('.path-input').value = ''; // clear current input await typeIn('.path-input', 'baz/bat'); assert.dom('.duplicate-path-error').exists(); assert.dom('.path-input').hasClass('error'); await clickTrigger('[data-test-variable-namespace-filter]'); await selectChoose( '[data-test-variable-namespace-filter]', server.db.namespaces[2].id ); assert.dom('.duplicate-path-error').doesNotExist(); assert.dom('.path-input').doesNotHaveClass('error'); document.querySelector('.path-input').value = ''; // clear current input await typeIn('.path-input', 'baz/bat/qux'); assert.dom('.duplicate-path-error').exists(); assert.dom('.path-input').hasClass('error'); }); test('warns you when you set a key with . in it', async function (assert) { this.set( 'mockedModel', server.create('variable', { keyValues: [{ key: '', value: '' }], }) ); const testCases = [ { name: 'valid key', key: 'superSecret2', warn: false, }, { name: 'invalid key with dot', key: 'super.secret', warn: true, }, { name: 'invalid key with slash', key: 'super/secret', warn: true, }, { name: 'invalid key with emoji', key: 'supersecretspy🕵️', warn: true, }, { name: 'unicode letters', key: '世界', warn: false, }, { name: 'unicode numbers', key: '٣٢١', warn: false, }, { name: 'unicode letters and numbers', key: '世٢界١', warn: false, }, ]; for (const tc of testCases) { await render(hbs``); await typeIn('[data-test-var-key]', tc.key); if (tc.warn) { assert.dom('.key-value-error').exists(tc.name); } else { assert.dom('.key-value-error').doesNotExist(tc.name); } } }); test('warns you when you create a duplicate key', async function (assert) { this.set( 'mockedModel', server.create('variable', { keyValues: [{ key: 'myKey', value: 'myVal' }], }) ); await render(hbs``); await click('[data-test-add-kv]'); const secondKey = document.querySelectorAll('[data-test-var-key]')[1]; await typeIn(secondKey, 'myWonderfulKey'); assert.dom('.key-value-error').doesNotExist(); secondKey.value = ''; await typeIn(secondKey, 'myKey'); assert.dom('.key-value-error').exists(); }); }); module('Views', function () { test('Allows you to swap between JSON and Key/Value Views', async function (assert) { this.set( 'mockedModel', server.create('variable', { path: '', keyValues: [{ key: '', value: '' }], }) ); this.set( 'existingVariables', server.createList('variable', 1, { path: 'baz/bat', }) ); this.set('view', 'table'); await render( hbs`` ); assert.dom('.key-value').exists(); assert.dom('.CodeMirror').doesNotExist(); this.set('view', 'json'); assert.dom('.key-value').doesNotExist(); assert.dom('.CodeMirror').exists(); }); test('Persists Key/Values table data to JSON', async function (assert) { faker.seed(1); assert.expect(2); const keyValues = [ { key: 'foo', value: '123' }, { key: 'bar', value: '456' }, ]; this.set( 'mockedModel', server.create('variable', { path: '', keyValues, }) ); this.set('view', 'json'); await render( hbs`` ); await percySnapshot(assert); const keyValuesAsJSON = keyValues.reduce((acc, { key, value }) => { acc[key] = value; return acc; }, {}); assert.equal( code('.editor-wrapper').get(), JSON.stringify(keyValuesAsJSON, null, 2), 'JSON editor contains the key values, stringified, by default' ); this.set('view', 'table'); await click('[data-test-add-kv]'); await typeIn('.key-value:last-of-type label:nth-child(1) input', 'howdy'); await typeIn( '.key-value:last-of-type label:nth-child(2) input', 'partner' ); this.set('view', 'json'); assert.ok( code('[data-test-json-editor]').get().includes('"howdy": "partner"'), 'JSON editor contains the new key value' ); }); test('Persists JSON data to Key/Values table', async function (assert) { const keyValues = [{ key: '', value: '' }]; this.set( 'mockedModel', server.create('variable', { path: '', keyValues, }) ); this.set('view', 'json'); await render( hbs`` ); codeFillable('[data-test-json-editor]').get()( JSON.stringify({ golden: 'gate' }, null, 2) ); this.set('view', 'table'); assert.equal( find(`.key-value:last-of-type label:nth-child(1) input`).value, 'golden', 'Key persists from JSON to Table' ); assert.equal( find(`.key-value:last-of-type label:nth-child(2) input`).value, 'gate', 'Value persists from JSON to Table' ); }); }); });