From e6ce95acd3ebd8543ff43ca9a9aa6c59f498884e Mon Sep 17 00:00:00 2001 From: Zack Moore Date: Wed, 23 Jul 2025 11:12:20 -0700 Subject: [PATCH] Replace codemirror 6 code editor with HDS CodeEditor component (#30188) * Completed initial replacement of editor * fixing ts issues * removing codemirror modifier and deps * working on replacing the code editor * addressing linting concerns * cleaning up policy-form editor * fixing linting issues * fixing linting issues * fixing tests * fixing tests * fixing tests * fixing tests * fixing failing tests * cleaning up PR * fixing tests * remove outdated message for navigating editor * fix linting in tests * add changelog * fix tests * update naming * remove unused lint param + name changes * update test selector usage * update test selector usage * update test selector usage * lint fixes * replace page object selectors * lint fix * fix lint * fix lint after merge * update tests * remove import --------- Co-authored-by: Lane Wetmore --- changelog/30188.txt | 3 + ui/app/components/generate-credentials.js | 10 +- ui/app/components/policy-form.hbs | 29 +-- ui/app/components/role-aws-edit.js | 12 +- ui/app/components/role-edit.js | 10 +- ui/app/components/secret-create-or-update.js | 34 +-- ui/app/components/secret-edit.js | 4 +- ui/app/components/tools/wrap.hbs | 2 +- ui/app/components/tools/wrap.ts | 13 +- .../vault/cluster/secrets/backend/sign.js | 10 +- ui/app/styles/components/codemirror.scss | 195 ------------------ .../styles/components/console-ui-panel.scss | 11 - ui/app/styles/core.scss | 1 - .../templates/components/console/log-json.hbs | 10 +- .../components/control-group-success.hbs | 11 +- .../components/form-field-from-model.hbs | 2 +- .../templates/components/oidc/scope-form.hbs | 4 - .../components/secret-create-or-update.hbs | 12 +- ui/app/templates/components/secret-edit.hbs | 2 +- .../templates/components/secret-form-show.hbs | 2 +- .../components/transit-key-action/export.hbs | 2 +- .../components/transit-key-action/hmac.hbs | 2 +- .../vault/cluster/secrets/backend/sign.hbs | 2 +- ui/ember-cli-build.js | 2 - ui/lib/core/addon/components/form-field.hbs | 5 +- ui/lib/core/addon/components/form-field.js | 12 +- ui/lib/core/addon/components/json-editor.hbs | 102 ++++----- ui/lib/core/addon/components/json-editor.js | 43 ++-- ui/lib/core/addon/modifiers/code-mirror.js | 92 --------- ui/lib/core/app/modifiers/code-mirror.js | 6 - .../components/page/role/create-and-edit.hbs | 2 +- .../components/page/role/create-and-edit.js | 13 +- ui/lib/kv/addon/components/kv-data-fields.hbs | 1 - ui/lib/kv/addon/components/kv-data-fields.js | 19 +- .../kv/addon/components/kv-patch/json-form.js | 11 +- ui/package.json | 2 - .../acceptance/auth/enable-tune-form-test.js | 4 +- .../oidc-config/providers-scopes-test.js | 38 ++-- ui/tests/acceptance/policies/index-test.js | 17 +- .../kv/kv-v2-workflow-edge-cases-test.js | 90 ++++---- .../secrets/backend/kv/secret-test.js | 65 +++--- ui/tests/acceptance/tools-test.js | 50 +++-- ui/tests/acceptance/transit-test.js | 15 +- ui/tests/helpers/codemirror.js | 71 ++++++- .../components/console/log-json-test.js | 10 +- .../components/json-editor-test.js | 110 +++------- .../page/role/create-and-edit-test.js | 80 +++---- .../components/kv/kv-data-fields-test.js | 38 ++-- .../components/kv/kv-patch/json-form-test.js | 29 ++- .../components/kv/page/kv-page-patch-test.js | 19 +- .../kv/page/kv-page-secret-edit-test.js | 32 ++- .../kv/page/kv-page-secrets-create-test.js | 22 +- .../ldap/page/role/create-and-edit-test.js | 34 ++- .../components/oidc/scope-form-test.js | 30 ++- .../components/policy-form-test.js | 66 ++---- .../search-select-with-modal-test.js | 19 +- .../components/secret-edit-test.js | 20 +- .../components/tools/unwrap-test.js | 16 +- .../integration/components/tools/wrap-test.js | 102 ++++++--- .../components/transit-key-actions-test.js | 36 +++- ui/tests/pages/components/json-editor.js | 12 +- ui/yarn.lock | 27 --- 62 files changed, 732 insertions(+), 1013 deletions(-) create mode 100644 changelog/30188.txt delete mode 100644 ui/app/styles/components/codemirror.scss delete mode 100644 ui/lib/core/addon/modifiers/code-mirror.js delete mode 100644 ui/lib/core/app/modifiers/code-mirror.js diff --git a/changelog/30188.txt b/changelog/30188.txt new file mode 100644 index 0000000000..9664a244d0 --- /dev/null +++ b/changelog/30188.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Use the Helios Design System Code Block component for all readonly code editors and use its Code Editor component for all other code editors +``` \ No newline at end of file diff --git a/ui/app/components/generate-credentials.js b/ui/app/components/generate-credentials.js index a69286538e..77794cf7bc 100644 --- a/ui/app/components/generate-credentials.js +++ b/ui/app/components/generate-credentials.js @@ -143,12 +143,12 @@ export default class GenerateCredentials extends Component { } @action - codemirrorUpdated(attr, val, codemirror) { - codemirror.performLint(); - const hasErrors = codemirror.state.lint.marked.length > 0; - - if (!hasErrors) { + editorUpdated(attr, val) { + // wont set invalid JSON to the model + try { this.model[attr] = JSON.parse(val); + } catch { + // linting is handled by the component } } diff --git a/ui/app/components/policy-form.hbs b/ui/app/components/policy-form.hbs index 19f5170f2a..10d6b1785e 100644 --- a/ui/app/components/policy-form.hbs +++ b/ui/app/components/policy-form.hbs @@ -23,13 +23,7 @@ {{/if}}
- {{#unless this.showFileUpload}} - - You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. - - {{/unless}} - {{#if @renderPolicyExampleModal}} {{! only true in policy create and edit routes }} @@ -43,9 +37,8 @@ /> {{/if}} - -
- {{#if @model.isNew}} + {{#if @model.isNew}} +
- {{else}} - {{! EDITING - no file upload toggle}} - - {{/if}} -
+
+ {{/if}}
{{#if this.showFileUpload}}
@@ -80,7 +61,6 @@ {{else}} {{/if}} -
{{#each @model.additionalAttrs as |attr|}} diff --git a/ui/app/components/role-aws-edit.js b/ui/app/components/role-aws-edit.js index 5423cc8859..8cbb530b74 100644 --- a/ui/app/components/role-aws-edit.js +++ b/ui/app/components/role-aws-edit.js @@ -43,12 +43,12 @@ export default RoleEdit.extend({ }); }, - codemirrorUpdated(attr, val, codemirror) { - codemirror.performLint(); - const hasErrors = codemirror.state.lint.marked.length > 0; - - if (!hasErrors) { - set(this.model, attr, val); + editorUpdated(attr, val) { + // wont set invalid JSON to the model + try { + set(this.model, attr, JSON.parse(val)); + } catch { + // linting is handled by the component } }, }, diff --git a/ui/app/components/role-edit.js b/ui/app/components/role-edit.js index a53601ecf8..b81088c6b8 100644 --- a/ui/app/components/role-edit.js +++ b/ui/app/components/role-edit.js @@ -102,12 +102,12 @@ export default Component.extend(FocusOnInsertMixin, { }); }, - codemirrorUpdated(attr, val, codemirror) { - codemirror.performLint(); - const hasErrors = codemirror.state.lint.marked.length > 0; - - if (!hasErrors) { + editorUpdated(attr, val) { + // wont set invalid JSON to the model + try { set(this.model, attr, JSON.parse(val)); + } catch { + // linting is handled by the component } }, }, diff --git a/ui/app/components/secret-create-or-update.js b/ui/app/components/secret-create-or-update.js index 1bc1e02acb..37d9ac4274 100644 --- a/ui/app/components/secret-create-or-update.js +++ b/ui/app/components/secret-create-or-update.js @@ -42,7 +42,7 @@ const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root'; const SHOW_ROUTE = 'vault.cluster.secrets.backend.show'; export default class SecretCreateOrUpdate extends Component { - @tracked codemirrorString = null; + @tracked editorString = null; @tracked error = null; @tracked secretPaths = null; @tracked pathWhiteSpaceWarning = false; @@ -58,7 +58,7 @@ export default class SecretCreateOrUpdate extends Component { @action setup(elem, [secretData, mode]) { - this.codemirrorString = secretData.toJSONString(); + this.editorString = secretData.toJSONString(); this.validationMessages = { path: '', }; @@ -160,21 +160,19 @@ export default class SecretCreateOrUpdate extends Component { } this.checkRows(); } + @action - codemirrorUpdated(val, codemirror) { - this.error = null; - codemirror.performLint(); - const noErrors = codemirror.state.lint.marked.length === 0; - if (noErrors) { - try { - this.args.secretData.fromJSONString(val); - set(this.args.modelForData, 'secretData', this.args.secretData.toJSON()); - } catch (e) { - this.error = e.message; - } + editorUpdated(val) { + try { + this.args.secretData.fromJSONString(val); + set(this.args.modelForData, 'secretData', this.args.secretData.toJSON()); + } catch (e) { + this.error = e.message; } - this.codemirrorString = val; + + this.editorString = val; } + @action createOrUpdateKey(type, event) { event.preventDefault(); @@ -204,21 +202,25 @@ export default class SecretCreateOrUpdate extends Component { this.checkRows(); this.handleChange(); } + @action formatJSON() { - this.codemirrorString = this.args.secretData.toJSONString(true); + this.editorString = this.args.secretData.toJSONString(true); } + @action handleMaskedInputChange(secret, index, value) { const row = { ...secret, value }; set(this.args.secretData, index, row); this.handleChange(); } + @action handleChange() { - this.codemirrorString = this.args.secretData.toJSONString(true); + this.editorString = this.args.secretData.toJSONString(true); set(this.args.modelForData, 'secretData', this.args.secretData.toJSON()); } + @action updateValidationErrorCount(errorCount) { this.validationErrorCount = errorCount; diff --git a/ui/app/components/secret-edit.js b/ui/app/components/secret-edit.js index efb3058496..ec7e5bdf02 100644 --- a/ui/app/components/secret-edit.js +++ b/ui/app/components/secret-edit.js @@ -35,13 +35,13 @@ export default class SecretEdit extends Component { @service store; @tracked secretData = null; - @tracked codemirrorString = null; + @tracked editorString = null; // fired on did-insert from render modifier @action createKvData(elem, [model]) { this.secretData = KVObject.create({ content: [] }).fromJSON(model.secretData); - this.codemirrorString = this.secretData.toJSONString(); + this.editorString = this.secretData.toJSONString(); } // TODO move this to the secret model @maybeQueryRecord( diff --git a/ui/app/components/tools/wrap.hbs b/ui/app/components/tools/wrap.hbs index 80ed1eb15b..21c60899f3 100644 --- a/ui/app/components/tools/wrap.hbs +++ b/ui/app/components/tools/wrap.hbs @@ -53,7 +53,7 @@ @title="Data to wrap" @subTitle="json-formatted" @value={{this.stringifiedWrapData}} - @valueUpdated={{this.codemirrorUpdated}} + @valueUpdated={{this.editorUpdated}} /> {{else}} 0; - if (!this.hasLintingErrors) this.wrapData = JSON.parse(val); + editorUpdated(val: string) { + this.hasLintingErrors = false; + + try { + this.wrapData = JSON.parse(val); + } catch { + this.hasLintingErrors = true; + } } @action diff --git a/ui/app/controllers/vault/cluster/secrets/backend/sign.js b/ui/app/controllers/vault/cluster/secrets/backend/sign.js index c25b9512e2..0a90d06d2e 100644 --- a/ui/app/controllers/vault/cluster/secrets/backend/sign.js +++ b/ui/app/controllers/vault/cluster/secrets/backend/sign.js @@ -19,12 +19,12 @@ export default Controller.extend({ }); }, - codemirrorUpdated(attr, val, codemirror) { - codemirror.performLint(); - const hasErrors = codemirror.state.lint.marked.length > 0; - - if (!hasErrors) { + editorUpdated(attr, val) { + // wont set invalid JSON to the model + try { set(this.model, attr, JSON.parse(val)); + } catch { + // linting is handled by the component } }, diff --git a/ui/app/styles/components/codemirror.scss b/ui/app/styles/components/codemirror.scss deleted file mode 100644 index f336410447..0000000000 --- a/ui/app/styles/components/codemirror.scss +++ /dev/null @@ -1,195 +0,0 @@ -@use 'sass:color'; -@use '../utils/color_variables'; -@use '../utils/font_variables'; -@use 'calendar-widget'; - -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -$light-grey: #dde3e7; -$light-gray: #a4a4a4; -$light-grey-blue: #6c7b81; -$dark-grey: #788290; -$faded-gray: #eaeaea; -// Product colors -$atlas: #127eff; -$vagrant: #2f88f7; -$consul: #69499a; -$terraform: #822ff7; -$serf: #dd4e58; -$packer: #1ddba3; - -// Our colors -$gray: color.adjust(color_variables.$black, $lightness: 89%); -color_variables.$red: #ff3d3d; -color_variables.$green: #39b54a; -calendar-widget.$dark-gray: #535f73; - -$gutter-grey: #2a2f36; - -.CodeMirror-lint-tooltip { - background-color: #f9f9fa; - border: 1px solid $light-gray; - border-radius: 0; - color: color.adjust(color_variables.$black, $lightness: 13%); - font-family: font_variables.$family-monospace; - font-size: 13px; - padding: 7px 8px 9px; -} - -.cm-s-hashi-read-only { - &.CodeMirror { - background-color: color_variables.$grey-lightest; - border: none; - color: color_variables.$ui-gray-600; - font-family: font_variables.$family-monospace; - -webkit-font-smoothing: auto; - line-height: 1.4; - } - - span.cm-string, - span.cm-string-2 { - color: $packer; - } - span.cm-property { - color: color.adjust($consul, $lightness: 20%); - } -} - -.cm-s-hashi { - &.CodeMirror { - background-color: color_variables.$black !important; - resize: vertical; - color: #cfd2d1 !important; - border: none; - font-family: font_variables.$family-monospace; - -webkit-font-smoothing: auto; - line-height: 1.4; - } - - .CodeMirror-gutters { - color: $dark-grey; - background-color: $gutter-grey; - border: none; - } - - .CodeMirror-cursor { - border-left: solid thin #f8f8f0; - } - - .CodeMirror-linenumber { - color: #6d8a88; - } - - div.CodeMirror-selected { - background: rgb(33, 66, 131); - } - - &.CodeMirror-focused div.CodeMirror-selected { - background: rgb(33, 66, 131); - } - - .CodeMirror-line::selection, - .CodeMirror-line > span::selection, - .CodeMirror-line > span > span::selection { - background: rgb(33, 66, 131); - } - - .CodeMirror-line::-moz-selection, - .CodeMirror-line > span::-moz-selection, - .CodeMirror-line > span > span::-moz-selection { - background: rgb(33, 66, 131); - } - - span.cm-comment { - color: $light-grey; - } - - span.cm-string, - span.cm-string-2 { - color: $packer; - } - - span.cm-number { - color: $serf; - } - - span.cm-variable { - color: color.adjust($consul, $lightness: 20%); - } - - span.cm-variable-2 { - color: color.adjust($consul, $lightness: 20%); - } - - span.cm-def { - color: $packer; - } - - span.cm-operator { - color: $gray; - } - span.cm-keyword { - color: color_variables.$yellow; - } - - span.cm-atom { - color: $serf; - } - - span.cm-meta { - color: $packer; - } - - span.cm-tag { - color: $packer; - } - - span.cm-attribute { - color: #9fca56; - } - - span.cm-qualifier { - color: #9fca56; - } - - span.cm-property { - color: color.adjust($consul, $lightness: 20%); - } - - span.cm-variable-3 { - color: #9fca56; - } - - span.cm-builtin { - color: #9fca56; - } - - .CodeMirror-activeline-background { - background: #101213; - } - - .CodeMirror-matchingbracket { - text-decoration: underline; - color: white !important; - } -} - -.readonly-codemirror { - .CodeMirror-code { - cursor: default; - } - .CodeMirror-cursor { - // https://github.com/codemirror/CodeMirror/issues/1099 - display: none; - } -} -.cm-s-auto-height.CodeMirror { - height: auto; -} - -.cm-s-short.CodeMirror { - height: 100px; -} diff --git a/ui/app/styles/components/console-ui-panel.scss b/ui/app/styles/components/console-ui-panel.scss index 75c1c67ae9..6244c1a9d2 100644 --- a/ui/app/styles/components/console-ui-panel.scss +++ b/ui/app/styles/components/console-ui-panel.scss @@ -46,17 +46,6 @@ $console-close-height: 35px; font-size: size_variables.$size-7; min-height: 2rem; padding: 0; - - &:not(.console-ui-command):not(.CodeMirror-line) { - padding-left: size_variables.$spacing-20; - } - } - - .cm-s-hashi.CodeMirror { - background-color: rgba(color_variables.$black, 0.5) !important; - font-weight: font_variables.$font-weight-normal; - margin-left: size_variables.$spacing-20; - padding: size_variables.$spacing-12 size_variables.$spacing-20; } .console-ui-panel-intro { diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 2bc6fcad1f..703e3bb6a1 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -59,7 +59,6 @@ @use 'components/chart-container'; @use 'components/clients-date-range'; @use 'components/cluster-banners'; -@use 'components/codemirror'; @use 'components/console-ui-panel'; @use 'components/control-group'; @use 'components/doc-link'; diff --git a/ui/app/templates/components/console/log-json.hbs b/ui/app/templates/components/console/log-json.hbs index 0a414890a1..48aa860223 100644 --- a/ui/app/templates/components/console/log-json.hbs +++ b/ui/app/templates/components/console/log-json.hbs @@ -4,15 +4,7 @@ }}
- +
- + {{/if}}
\ No newline at end of file diff --git a/ui/app/templates/components/oidc/scope-form.hbs b/ui/app/templates/components/oidc/scope-form.hbs index 14b36c92aa..6b194e3241 100644 --- a/ui/app/templates/components/oidc/scope-form.hbs +++ b/ui/app/templates/components/oidc/scope-form.hbs @@ -44,10 +44,6 @@ {{#each @model.formFields as |field|}} {{/each}} -

- You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field. Click 'How to write JSON - template for scopes' to view an example. -

{{else}} @@ -150,9 +150,9 @@
{{else}} diff --git a/ui/app/templates/components/secret-edit.hbs b/ui/app/templates/components/secret-edit.hbs index 598e7222d2..a9cd9587ca 100644 --- a/ui/app/templates/components/secret-edit.hbs +++ b/ui/app/templates/components/secret-edit.hbs @@ -51,7 +51,7 @@ @showAdvancedMode={{this.showAdvancedMode}} @modelForData={{this.modelForData}} @canUpdateSecret={{this.canUpdateSecret}} - @codemirrorString={{this.codemirrorString}} + @editorString={{this.editorString}} @editActions={{hash toggleAdvanced=(action "toggleAdvanced") refresh=(action "refresh")}} /> diff --git a/ui/app/templates/components/secret-form-show.hbs b/ui/app/templates/components/secret-form-show.hbs index 8e034022cf..70bef51507 100644 --- a/ui/app/templates/components/secret-form-show.hbs +++ b/ui/app/templates/components/secret-form-show.hbs @@ -12,7 +12,7 @@ {{else}} {{#if @showAdvancedMode}}
- +
{{else}}
diff --git a/ui/app/templates/components/transit-key-action/export.hbs b/ui/app/templates/components/transit-key-action/export.hbs index 07b7cfba26..606855a4c6 100644 --- a/ui/app/templates/components/transit-key-action/export.hbs +++ b/ui/app/templates/components/transit-key-action/export.hbs @@ -63,7 +63,7 @@
- +
diff --git a/ui/app/templates/components/transit-key-action/hmac.hbs b/ui/app/templates/components/transit-key-action/hmac.hbs index 2622febdc6..ee0524b69f 100644 --- a/ui/app/templates/components/transit-key-action/hmac.hbs +++ b/ui/app/templates/components/transit-key-action/hmac.hbs @@ -46,7 +46,7 @@
- +
diff --git a/ui/app/templates/vault/cluster/secrets/backend/sign.hbs b/ui/app/templates/vault/cluster/secrets/backend/sign.hbs index c432d903a0..c22cfbe0ce 100644 --- a/ui/app/templates/vault/cluster/secrets/backend/sign.hbs +++ b/ui/app/templates/vault/cluster/secrets/backend/sign.hbs @@ -94,7 +94,7 @@ @model={{this.model}} @updateTtl={{action "updateTtl" attr.name}} @emptyData={{this.emptyData}} - @codemirrorUpdated={{action "codemirrorUpdated" attr.name}} + @editorUpdated={{action "editorUpdated" attr.name}} /> {{/if}} {{/each}} diff --git a/ui/ember-cli-build.js b/ui/ember-cli-build.js index 8a8c3afc86..6f050520c9 100644 --- a/ui/ember-cli-build.js +++ b/ui/ember-cli-build.js @@ -80,8 +80,6 @@ module.exports = function (defaults) { const app = new EmberApp(defaults, appConfig); app.import('node_modules/jsonlint/lib/jsonlint.js'); - app.import('node_modules/codemirror/addon/lint/lint.css'); - app.import('node_modules/codemirror/lib/codemirror.css'); app.import('node_modules/text-encoder-lite/text-encoder-lite.js'); app.import('node_modules/jsondiffpatch/dist/jsondiffpatch.umd.js'); app.import('node_modules/jsondiffpatch/dist/formatters-styles/html.css'); diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs index 2dbc31b997..273648a36f 100644 --- a/ui/lib/core/addon/components/form-field.hbs +++ b/ui/lib/core/addon/components/form-field.hbs @@ -484,8 +484,7 @@ (if (eq @attr.options.mode "ruby") value (stringify (jsonify value))) @attr.options.defaultValue }} - @valueUpdated={{fn this.codemirrorUpdated true}} - @theme={{or @attr.options.theme "hashi"}} + @valueUpdated={{fn this.editorUpdated true}} @helpText={{this.helpTextString}} @mode={{@attr.options.mode}} @example={{@attr.options.example}} @@ -519,7 +518,7 @@ diff --git a/ui/lib/core/addon/components/form-field.js b/ui/lib/core/addon/components/form-field.js index 9f018298fd..70c699c2b6 100644 --- a/ui/lib/core/addon/components/form-field.js +++ b/ui/lib/core/addon/components/form-field.js @@ -247,16 +247,18 @@ export default class FormFieldComponent extends Component { this.setAndBroadcast(`${valueToSet}`); } @action - codemirrorUpdated(isString, value, codemirror) { - codemirror.performLint(); - const hasErrors = codemirror.state.lint.marked.length > 0; - const valToSet = isString ? value : JSON.parse(value); + editorUpdated(isString, value) { + try { + const valToSet = isString ? value : JSON.parse(value); - if (!hasErrors) { this.args.model.set(this.valuePath, valToSet); + this.onChange(this.valuePath, valToSet); + } catch { + // if the value is not valid JSON, we don't want to set it on the model } } + @action toggleTextShow() { const value = !this.showToggleTextInput; diff --git a/ui/lib/core/addon/components/json-editor.hbs b/ui/lib/core/addon/components/json-editor.hbs index 8b10ea1905..175b7241c2 100644 --- a/ui/lib/core/addon/components/json-editor.hbs +++ b/ui/lib/core/addon/components/json-editor.hbs @@ -2,19 +2,58 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: BUSL-1.1 }} -
- {{#if this.getShowToolbar}} -
- - - + + {{/if}} + + {{#if @helpText}} + + {{@helpText}} + + {{/if}} + + {{else}} + + {{#if this.getShowToolbar}} + {{#if @title}} + + {{@title}} + + {{/if}} + + {{#if @subTitle}} + + {{@subTitle}} + + {{/if}} + + {{#if @helpText}} + + {{@helpText}} + + {{/if}} + + {{yield}} + {{#if @example}} {{/if}} -
- -
-
-
- {{/if}} -
- - {{#if @helpText}} -
-

{{@helpText}}

-
+ + {{/if}} + {{/if}}
\ No newline at end of file diff --git a/ui/lib/core/addon/components/json-editor.js b/ui/lib/core/addon/components/json-editor.js index 88b36ef8ff..704e8c09dc 100644 --- a/ui/lib/core/addon/components/json-editor.js +++ b/ui/lib/core/addon/components/json-editor.js @@ -14,32 +14,40 @@ import { action } from '@ember/object'; * * @param {string} [title] - Name above codemirror view * @param {boolean} [showToolbar=true] - If false, toolbar and title are hidden - * @param {string} value - a specific string the comes from codemirror. It's the value inside the codemirror display + * @param {string} [value] - a specific string the comes from codemirror. It's the value inside the codemirror display * @param {Function} [valueUpdated] - action to preform when you edit the codemirror value. - * @param {Function} [onFocusOut] - action to preform when you focus out of codemirror. + * @param {Function} [onBlur] - action to preform when you focus out of codemirror. * @param {string} [helpText] - helper text. * @param {Object} [extraKeys] - Provides keyboard shortcut methods for things like saving on shift + enter. - * @param {Array} [gutters] - An array of CSS class names or class name / CSS string pairs, each of which defines a width (and optionally a background), and which will be used to draw the background of the gutters. - * @param {string} [mode] - The mode defined for styling. Right now we only import ruby so mode must but be ruby or defaults to javascript. If you wanted another language you need to import it into the modifier. + * @param {string} [mode] - The mode defined for styling * @param {Boolean} [readOnly] - Sets the view to readOnly, allowing for copying but no editing. It also hides the cursor. Defaults to false. - * @param {String} [theme] - Specify or customize the look via a named "theme" class in scss. * @param {String} [value] - Value within the display. Generally, a json string. - * @param {String} [viewportMargin] - Specifies the amount of lines rendered on the DOM (this is not the editor display height). The codemirror default is 10 which we set explicity in the code-mirror modifier per the recommendations from the codemirror docs. * @param {string} [example] - Example to show when value is null -- when example is provided a restore action will render in the toolbar to clear the current value and show the example after input - * @param {string} [screenReaderLabel] - This label is read by the screen readers when CodeMirror text area is focused. This is helpful for accessibility. * @param {string} [container] - **REQUIRED if rendering within a modal** Selector string or element object of containing element, set the focused element as the container value. This is for the Hds::Copy::Button and to set `autoRefresh=true` so content renders https://hds-website-hashicorp.vercel.app/components/copy/button?tab=code + * @param {Function} [onSetup] - action to preform when the codemirror editor is setup. * */ export default class JsonEditorComponent extends Component { + _codemirrorEditor = null; + + get mode() { + return this.args.mode ?? 'json'; + } + get getShowToolbar() { - return this.args.showToolbar === false ? false : true; + return this.args.showToolbar ?? true; + } + + get ariaLabel() { + return this.args.title ?? 'JSON Editor'; } @action onSetup(editor) { - // store reference to codemirror editor so that it can be passed to valueUpdated when restoring example this._codemirrorEditor = editor; + + this.args.onSetup?.(editor); } @action @@ -50,16 +58,17 @@ export default class JsonEditorComponent extends Component { } } - @action - onFocus(...args) { - if (this.args.onFocusOut) { - this.args.onFocusOut(...args); - } - } - @action restoreExample() { - // set value to null which will cause the example value to be passed into the editor + this._codemirrorEditor.dispatch({ + changes: [ + { + from: 0, + to: this._codemirrorEditor.state.doc.length, + insert: this.args.example, + }, + ], + }); this.args.valueUpdated(null, this._codemirrorEditor); } } diff --git a/ui/lib/core/addon/modifiers/code-mirror.js b/ui/lib/core/addon/modifiers/code-mirror.js deleted file mode 100644 index 8b6c73f667..0000000000 --- a/ui/lib/core/addon/modifiers/code-mirror.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { action } from '@ember/object'; -import { bind } from '@ember/runloop'; -import codemirror from 'codemirror'; -import Modifier from 'ember-modifier'; -import { stringify } from 'core/helpers/stringify'; - -import 'codemirror/addon/edit/matchbrackets'; -import 'codemirror/addon/selection/active-line'; -import 'codemirror/addon/display/autorefresh'; -import 'codemirror/addon/lint/lint.js'; -import 'codemirror/addon/lint/json-lint.js'; -// right now we only use the ruby and javascript, if you use another mode you'll need to import it. -// https://codemirror.net/mode/ -import 'codemirror/mode/ruby/ruby'; -import 'codemirror/mode/javascript/javascript'; - -export default class CodeMirrorModifier extends Modifier { - modify(element, positionalArgs, namedArgs) { - // setup codemirror initially when modifier is installed on the element - if (!this._editor) { - this._setup(element, namedArgs); - } else { - // this hook also fires any time there is a change to tracked state - this._editor.setOption('readOnly', namedArgs.readOnly); - let value = this._editor.getValue(); - let content = namedArgs.content; - if (!content) return; - try { - // First parse json to make white space and line breaks consistent between the two items, - // then stringify so they can be compared. - // We use the stringify helper so we do not flatten the json object - value = stringify([JSON.parse(value)], {}); - content = stringify([JSON.parse(content)], {}); - } catch { - // this catch will occur for non-json content when the mode is not javascript (e.g. ruby). - } - if (value !== content) { - this._editor.setValue(namedArgs.content); - } - } - } - - @action - _onChange(namedArgs, editor) { - // avoid sending change event after initial setup when editor value is set to content - if (namedArgs.content !== editor.getValue()) { - namedArgs.onUpdate(editor.getValue(), this._editor); - } - } - - @action - _onFocus(namedArgs, editor) { - namedArgs.onFocus(editor.getValue()); - } - - _setup(element, namedArgs) { - const editor = codemirror(element, { - // IMPORTANT: `gutters` must come before `lint` since the presence of - // `gutters` is cached internally when `lint` is toggled - gutters: namedArgs.gutters || ['CodeMirror-lint-markers'], - matchBrackets: true, - lint: { lintOnChange: true }, - showCursorWhenSelecting: true, - styleActiveLine: true, - tabSize: 2, - // all values we can pass into the JsonEditor - screenReaderLabel: namedArgs.screenReaderLabel || '', - extraKeys: namedArgs.extraKeys || '', - lineNumbers: namedArgs.lineNumbers, - mode: namedArgs.mode || 'application/json', - readOnly: namedArgs.readOnly || false, - theme: namedArgs.theme || 'hashi', - value: namedArgs.content || '', - viewportMargin: namedArgs.viewportMargin || 10, - autoRefresh: namedArgs.autoRefresh, - }); - - editor.on('change', bind(this, this._onChange, namedArgs)); - editor.on('focus', bind(this, this._onFocus, namedArgs)); - - this._editor = editor; - - if (namedArgs.onSetup) { - namedArgs.onSetup(editor); - } - } -} diff --git a/ui/lib/core/app/modifiers/code-mirror.js b/ui/lib/core/app/modifiers/code-mirror.js deleted file mode 100644 index 4410583457..0000000000 --- a/ui/lib/core/app/modifiers/code-mirror.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -export { default } from 'core/modifiers/code-mirror'; diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs index 317fd444a6..7965302776 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs @@ -115,12 +115,12 @@ @mode="ruby" @valueUpdated={{fn (mut template.rules)}} @helpText={{sanitized-html this.roleRulesHelpText}} + @onSetup={{fn (mut this.codemirrorEditor)}} > diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js index 07c68fe9f8..4fe70b5f21 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js @@ -28,6 +28,7 @@ export default class CreateAndEditRolePageComponent extends Component { @tracked modelValidations; @tracked invalidFormAlert; @tracked errorBanner; + @tracked codemirrorEditor; constructor() { super(...arguments); @@ -85,7 +86,7 @@ export default class CreateAndEditRolePageComponent extends Component { const message = 'This specifies the Role or ClusterRole rules to use when generating a role. Kubernetes documentation is'; const link = - 'available here'; + 'available here'; return `${message} ${link}.`; } @@ -112,6 +113,16 @@ export default class CreateAndEditRolePageComponent extends Component { @action resetRoleRules() { this.roleRulesTemplates = getRules(); + + this.codemirrorEditor.dispatch({ + changes: [ + { + from: 0, + to: this.codemirrorEditor.state.doc.length, + insert: this.args.value, + }, + ], + }); } @action diff --git a/ui/lib/kv/addon/components/kv-data-fields.hbs b/ui/lib/kv/addon/components/kv-data-fields.hbs index c3664ff35e..4f2cb19711 100644 --- a/ui/lib/kv/addon/components/kv-data-fields.hbs +++ b/ui/lib/kv/addon/components/kv-data-fields.hbs @@ -31,7 +31,6 @@ @title="{{if (eq @type 'create') 'Secret' 'Version'}} data" @value={{this.stringifiedSecretData}} @valueUpdated={{this.handleJson}} - @viewportMargin={{this.viewportMargin}} /> {{/if}} {{#if (or @modelValidations.secretData.errors this.lintingErrors)}} diff --git a/ui/lib/kv/addon/components/kv-data-fields.js b/ui/lib/kv/addon/components/kv-data-fields.js index cdd792bc00..d42a99fe9a 100644 --- a/ui/lib/kv/addon/components/kv-data-fields.js +++ b/ui/lib/kv/addon/components/kv-data-fields.js @@ -41,21 +41,14 @@ export default class KvDataFields extends Component { return this.args.secret?.secretData ? stringify([this.args.secret.secretData], {}) : this.startingValue; } - get viewportMargin() { - if (!this.args?.secret?.secretData) return 10; - const jsonHeight = Object.keys(this.args.secret.secretData).length; - // return the higher of: 10 or the approimated number of lines in the json. jsonHeight only includes the first level of keys, so for objects - // with lots of nested values, it will undercount. - const max = Math.max(jsonHeight, 10); - return Math.min(max, 1000); // cap at 1000 lines to avoid performance implications - } - @action - handleJson(value, codemirror) { - codemirror.performLint(); - this.lintingErrors = codemirror.state.lint.marked.length > 0; - if (!this.lintingErrors) { + handleJson(value) { + this.lintingErrors = false; + + try { this.args.secret.secretData = JSON.parse(value); + } catch { + this.lintingErrors = true; } } } diff --git a/ui/lib/kv/addon/components/kv-patch/json-form.js b/ui/lib/kv/addon/components/kv-patch/json-form.js index 718c41041d..54e5c0052c 100644 --- a/ui/lib/kv/addon/components/kv-patch/json-form.js +++ b/ui/lib/kv/addon/components/kv-patch/json-form.js @@ -33,11 +33,14 @@ export default class KvPatchJsonForm extends Component { } @action - handleJson(value, codemirror) { - codemirror.performLint(); - this.lintingErrors = codemirror.state.lint.marked.length > 0; - if (!this.lintingErrors) { + handleJson(value) { + this.lintingErrors = false; + + try { + JSON.parse(value); this.jsonObject = value; + } catch { + this.lintingErrors = true; } } diff --git a/ui/package.json b/ui/package.json index 8b10cd7f42..25349ec56a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -64,7 +64,6 @@ "@icholy/duration": "~5.1.0", "@lineal-viz/lineal": "~0.5.1", "@tsconfig/ember": "~2.0.0", - "@types/codemirror": "~5.60.15", "@types/d3-array": "~3.2.1", "@types/ember-data": "~4.4.16", "@types/qunit": "~2.19.12", @@ -78,7 +77,6 @@ "base64-js": "~1.5.1", "broccoli-asset-rev": "~3.0.0", "broccoli-sri-hash": "meirish/broccoli-sri-hash#rooturl", - "codemirror": "~5.65.19", "columnify": "~1.6.0", "concurrently": "~9.1.2", "d3-array": "~3.2.4", diff --git a/ui/tests/acceptance/auth/enable-tune-form-test.js b/ui/tests/acceptance/auth/enable-tune-form-test.js index 07d192fbe8..9015f98492 100644 --- a/ui/tests/acceptance/auth/enable-tune-form-test.js +++ b/ui/tests/acceptance/auth/enable-tune-form-test.js @@ -68,7 +68,7 @@ module('Acceptance | auth enable tune form test', function (hooks) { this.type = 'jwt'; this.path = `${this.type}-${uuidv4()}`; this.customSelectors = { - providerConfig: `${GENERAL.fieldByAttr('providerConfig')} textarea`, + providerConfig: `${GENERAL.fieldByAttr('providerConfig')} .cm-editor`, }; this.tuneFields = [ 'defaultRole', @@ -156,7 +156,7 @@ module('Acceptance | auth enable tune form test', function (hooks) { this.type = 'oidc'; this.path = `${this.type}-${uuidv4()}`; this.customSelectors = { - providerConfig: `${GENERAL.fieldByAttr('providerConfig')} textarea`, + providerConfig: `${GENERAL.fieldByAttr('providerConfig')} .cm-editor`, }; this.tuneFields = [ 'oidcDiscoveryUrl', diff --git a/ui/tests/acceptance/oidc-config/providers-scopes-test.js b/ui/tests/acceptance/oidc-config/providers-scopes-test.js index 6aab93dc9b..465e8781d9 100644 --- a/ui/tests/acceptance/oidc-config/providers-scopes-test.js +++ b/ui/tests/acceptance/oidc-config/providers-scopes-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { visit, currentURL, click, fillIn, findAll, currentRouteName } from '@ember/test-helpers'; +import { find, visit, currentURL, click, fillIn, findAll, currentRouteName } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import oidcConfigHandlers from 'vault/mirage/handlers/oidc-config'; @@ -48,9 +48,9 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) { assert.expect(4); this.server.get('/identity/oidc/scope', () => overrideResponse(404)); await visit(OIDC_BASE_URL); - await click('[data-test-tab="scopes"]'); + await click(GENERAL.tab('scopes')); assert.strictEqual(currentURL(), '/vault/access/oidc/scopes'); - assert.dom('[data-test-tab="scopes"]').hasClass('active', 'scopes tab is active'); + assert.dom(GENERAL.tab('scopes')).hasClass('active', 'scopes tab is active'); assert .dom(SELECTORS.scopeEmptyState) .hasText( @@ -176,9 +176,21 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) { 'vault.cluster.access.oidc.scopes.create', 'navigates to create form' ); - await fillIn('[data-test-input="name"]', 'test-scope'); - await fillIn('[data-test-input="description"]', 'this is a test'); - await fillIn('[data-test-component="code-mirror-modifier"] textarea', SCOPE_DATA_RESPONSE.template); + await fillIn(GENERAL.inputByAttr('name'), 'test-scope'); + await fillIn(GENERAL.inputByAttr('description'), 'this is a test'); + + const editorElement = await find(`${GENERAL.codemirror} .hds-code-editor__editor`); + const { editor } = editorElement; + + editor.dispatch({ + changes: [ + { + from: 0, + to: editor.state.doc.length, + insert: SCOPE_DATA_RESPONSE.template, + }, + ], + }); await click(SELECTORS.scopeSaveButton); assert.strictEqual( flashMessage.latestMessage, @@ -203,7 +215,7 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) { 'vault.cluster.access.oidc.scopes.scope.edit', 'navigates to edit page from details' ); - await fillIn('[data-test-input="description"]', 'this is an edit test'); + await fillIn(GENERAL.inputByAttr('description'), 'this is an edit test'); await click(SELECTORS.scopeSaveButton); assert.strictEqual( flashMessage.latestMessage, @@ -221,15 +233,15 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) { // create a provider using test-scope await click('[data-test-breadcrumb-link="oidc-scopes"] a'); - await click('[data-test-tab="providers"]'); - assert.dom('[data-test-tab="providers"]').hasClass('active', 'providers tab is active'); + await click(GENERAL.tab('providers')); + assert.dom(GENERAL.tab('providers')).hasClass('active', 'providers tab is active'); await click('[data-test-oidc-provider-create]'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.create', 'navigates to provider create form' ); - await fillIn('[data-test-input="name"]', 'test-provider'); + await fillIn(GENERAL.inputByAttr('name'), 'test-provider'); await clickTrigger('#scopesSupported'); await selectChoose('#scopesSupported', 'test-scope'); await click(SELECTORS.providerSaveButton); @@ -271,7 +283,7 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) { ); await click('[data-test-oidc-radio="limited"]'); await click('[data-test-component="search-select"]#allowedClientIds .ember-basic-dropdown-trigger'); - await fillIn('.ember-power-select-search input', 'test-app'); + await fillIn(GENERAL.searchSelect.searchInput, 'test-app'); await searchSelect.options.objectAt(0).click(); await click(SELECTORS.providerSaveButton); assert.strictEqual( @@ -342,8 +354,8 @@ module('Acceptance | oidc-config providers and scopes', function (hooks) { test('it lists default provider and navigates to details', async function (assert) { assert.expect(7); await visit(OIDC_BASE_URL); - await click('[data-test-tab="providers"]'); - assert.dom('[data-test-tab="providers"]').hasClass('active', 'providers tab is active'); + await click(GENERAL.tab('providers')); + assert.dom(GENERAL.tab('providers')).hasClass('active', 'providers tab is active'); assert.strictEqual(currentURL(), '/vault/access/oidc/providers'); assert .dom('[data-test-oidc-provider-linked-block="default"]') diff --git a/ui/tests/acceptance/policies/index-test.js b/ui/tests/acceptance/policies/index-test.js index e43421b876..4561c485f2 100644 --- a/ui/tests/acceptance/policies/index-test.js +++ b/ui/tests/acceptance/policies/index-test.js @@ -19,7 +19,7 @@ import { v4 as uuidv4 } from 'uuid'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { runCmd } from 'vault/tests/helpers/commands'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; const SELECT = { @@ -27,7 +27,6 @@ const SELECT = { filterBar: '[data-test-component="navigate-input"]', createPolicy: '[data-test-policy-create-link]', nameInput: '[data-test-policy-input="name"]', - createError: '[data-test-message-error]', policyTitle: '[data-test-policy-name]', listBreadcrumb: '[data-test-policy-list-link] a', }; @@ -85,7 +84,11 @@ module('Acceptance | policies/acl', function (hooks) { await click(SELECT.createPolicy); await fillIn(SELECT.nameInput, policyName); - codemirror().setValue(policyString); + + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, policyString); + await click(GENERAL.submitButton); assert.strictEqual( currentURL(), @@ -107,9 +110,13 @@ module('Acceptance | policies/acl', function (hooks) { await fillIn(SELECT.nameInput, policyName); await click(GENERAL.submitButton); assert - .dom(SELECT.createError) + .dom(GENERAL.messageError) .hasText(`Error 'policy' parameter not supplied or empty`, 'renders error message on save'); - codemirror().setValue(policyString); + + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, policyString); + await click(GENERAL.submitButton); await waitUntil(() => currentURL() === `/vault/policy/acl/${encodeURIComponent(policyLower)}`); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js index 9df04b0ee6..98081e549b 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-edge-cases-test.js @@ -13,7 +13,7 @@ import { setupOnerror, typeIn, visit, - triggerKeyEvent, + waitFor, } from '@ember/test-helpers'; import { setupApplicationTest } from 'vault/tests/helpers'; import { login, loginNs } from 'vault/tests/helpers/auth/auth-helpers'; @@ -36,7 +36,7 @@ import { clearRecords, writeSecret, writeVersionedSecret } from 'vault/tests/hel import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { personas } from 'vault/tests/helpers/kv/policy-generator'; import { capabilitiesStub } from 'vault/tests/helpers/stubs'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -97,7 +97,7 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { .dom(GENERAL.submitButton) .hasText('View list', 'shows list and not secret because search is a directory'); await click(GENERAL.submitButton); - assert.dom(PAGE.emptyStateTitle).hasText(`There are no secrets matching "${root}/no-access/".`); + assert.dom(GENERAL.emptyStateTitle).hasText(`There are no secrets matching "${root}/no-access/".`); await visit(`/vault/secrets/${backend}/kv/list`); await typeIn(PAGE.list.overviewInput, `${root}/`); // add slash because this is a directory @@ -121,10 +121,10 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { assert.dom(PAGE.toolbarAction).exists({ count: 1 }, 'toolbar only renders create secret action'); assert.dom(PAGE.list.filter).hasValue(`${root}/`); // List content correct - assert.dom(PAGE.list.item(`${subdirectory}/`)).exists('renders linked block for subdirectory'); - await click(PAGE.list.item(`${subdirectory}/`)); - assert.dom(PAGE.list.item(secret)).exists('renders linked block for child secret'); - await click(PAGE.list.item(secret)); + assert.dom(GENERAL.listItem(`${subdirectory}/`)).exists('renders linked block for subdirectory'); + await click(GENERAL.listItem(`${subdirectory}/`)); + assert.dom(GENERAL.listItem(secret)).exists('renders linked block for child secret'); + await click(GENERAL.listItem(secret)); assert .dom(GENERAL.overviewCard.container('Current version')) .hasText(`Current version The current version of this secret. 1`); @@ -148,29 +148,29 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { await visit(`vault/secrets/${backend}/kv/${encodeURIComponent(this.fullSecretPath)}/details?version=1`); // navigate back through crumbs - let previousCrumb = findAll('[data-test-breadcrumbs] li').length - 2; - await click(PAGE.breadcrumbAtIdx(previousCrumb)); + let previousCrumb = findAll(GENERAL.breadcrumb).length - 2; + await click(GENERAL.breadcrumbAtIdx(previousCrumb)); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kv/list/${root}/${subdirectory}/`, 'goes back to subdirectory list' ); assert.dom(PAGE.list.filter).hasValue(`${root}/${subdirectory}/`); - assert.dom(PAGE.list.item(secret)).exists('renders linked block for child secret'); + assert.dom(GENERAL.listItem(secret)).exists('renders linked block for child secret'); // back again - previousCrumb = findAll('[data-test-breadcrumbs] li').length - 2; - await click(PAGE.breadcrumbAtIdx(previousCrumb)); + previousCrumb = findAll(GENERAL.breadcrumb).length - 2; + await click(GENERAL.breadcrumbAtIdx(previousCrumb)); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kv/list/${root}/`, 'goes back to root directory' ); - assert.dom(PAGE.list.item(`${subdirectory}/`)).exists('renders linked block for subdirectory'); + assert.dom(GENERAL.listItem(`${subdirectory}/`)).exists('renders linked block for subdirectory'); // and back to the engine list view - previousCrumb = findAll('[data-test-breadcrumbs] li').length - 2; - await click(PAGE.breadcrumbAtIdx(previousCrumb)); + previousCrumb = findAll(GENERAL.breadcrumb).length - 2; + await click(GENERAL.breadcrumbAtIdx(previousCrumb)); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kv/list`, @@ -194,8 +194,8 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { `Sorry, we were unable to find any content at /v1/${backend}/metadata/${root}/${subdirectory}.` ); - assert.dom(PAGE.breadcrumbAtIdx(0)).hasText('Secrets'); - assert.dom(PAGE.breadcrumbAtIdx(1)).hasText(backend); + assert.dom(GENERAL.breadcrumbAtIdx(0)).hasText('Secrets'); + assert.dom(GENERAL.breadcrumbAtIdx(1)).hasText(backend); assert.dom(PAGE.secretTab('Secrets')).doesNotHaveClass('is-active'); assert.dom(PAGE.secretTab('Configuration')).doesNotHaveClass('is-active'); }); @@ -306,13 +306,13 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { test('no ghost item after editing metadata', async function (assert) { await visit(`/vault/secrets/${this.backend}/kv/list/edge/`); assert.dom(PAGE.list.item()).exists({ count: 2 }, 'two secrets are listed'); - await click(PAGE.list.item('two')); + await click(GENERAL.listItem('two')); await click(PAGE.secretTab('Metadata')); await click(PAGE.metadata.editBtn); await fillIn(FORM.keyInput(), 'foo'); await fillIn(FORM.valueInput(), 'bar'); await click(FORM.saveBtn); - await click(PAGE.breadcrumbAtIdx(2)); + await click(GENERAL.breadcrumbAtIdx(2)); assert.dom(PAGE.list.item()).exists({ count: 2 }, 'two secrets are listed'); }); @@ -322,14 +322,17 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { await click(GENERAL.toggleInput('json')); + await waitFor('.cm-editor'); + const view = codemirror(); + assert.strictEqual( - codemirror().getValue(), + getCodeEditorValue(view), `{ \"\": \"\" }`, 'JSON editor displays correct empty object' ); - codemirror().setValue('{ "foo3": { "name": "bar3" } }'); + setCodeEditorValue(view, '{ "foo3": { "name": "bar3" } }'); await click(FORM.saveBtn); // Details view @@ -346,29 +349,12 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { assert.dom(GENERAL.toggleInput('json')).isNotDisabled(); assert.dom(GENERAL.toggleInput('json')).isChecked(); assert.deepEqual( - codemirror().getValue(), - `{ - "foo3": { - "name": "bar3" - } -}`, + getCodeEditorValue(view), + '{ "foo3": { "name": "bar3" } }', 'Values are displayed in the new version view' ); }); - test('on enter the JSON editor cursor goes to the next line', async function (assert) { - // see issue here: https://github.com/hashicorp/vault/issues/27524 - const predictedCursorPosition = JSON.stringify({ line: 3, ch: 0, sticky: null }); - await visit(`/vault/secrets/${this.backend}/kv/create`); - await fillIn(FORM.inputByAttr('path'), 'json jump'); - - await click(GENERAL.toggleInput('json')); - codemirror().setCursor({ line: 2, ch: 1 }); - await triggerKeyEvent(GENERAL.codemirrorTextarea, 'keydown', 'Enter'); - const actualCursorPosition = JSON.stringify(codemirror().getCursor()); - assert.strictEqual(actualCursorPosition, predictedCursorPosition, 'the cursor stayed on the next line'); - }); - test('viewing advanced secret data versions displays the correct version data', async function (assert) { assert.expect(2); const expectedDataV1 = `{ @@ -382,16 +368,22 @@ module('Acceptance | kv-v2 workflow | edge cases', function (hooks) { } }`; + let view; + await visit(`/vault/secrets/${this.backend}/kv/create`); await fillIn(FORM.inputByAttr('path'), 'complex_version_test'); await click(GENERAL.toggleInput('json')); - codemirror().setValue('{ "foo1": { "name": "bar1" } }'); + await waitFor('.cm-editor'); + view = codemirror(); + setCodeEditorValue(view, '{ "foo1": { "name": "bar1" } }'); await click(FORM.saveBtn); // Create another version await click(GENERAL.overviewCard.actionText('Create new')); - codemirror().setValue('{ "foo2": { "name": "bar2" } }'); + await waitFor('.cm-editor'); + view = codemirror(); + setCodeEditorValue(view, '{ "foo2": { "name": "bar2" } }'); await click(FORM.saveBtn); // View the first version and make sure the secret data is correct @@ -520,7 +512,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) setupApplicationTest(hooks); const navToEngine = async (backend) => { - await click('[data-test-sidebar-nav-link="Secrets Engines"]'); + await click(GENERAL.navLink('Secrets Engines')); return await click(SES.secretsBackendLink(backend)); }; @@ -542,7 +534,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) }); // also asserts destroyed icon deleted.forEach((num) => { - assert.dom(`${PAGE.detail.version(num)} [data-test-icon="x-square"]`); + assert.dom(`${PAGE.detail.version(num)} ${GENERAL.icon('x-square')}`); }); }; @@ -624,7 +616,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) ); await assertVersionDropdown(assert); assert - .dom(`${PAGE.detail.version(2)} [data-test-icon="check-circle"]`) + .dom(`${PAGE.detail.version(2)} ${GENERAL.icon('check-circle')}`) .exists('renders current version icon'); assert.dom(PAGE.infoRowValue('foo-two')).hasText('***********'); await click(PAGE.infoRowToggleMasked('foo-two')); @@ -664,7 +656,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) // check empty state and toolbar assertDeleteActions(assert, ['undelete', 'destroy']); assert - .dom(PAGE.emptyStateTitle) + .dom(GENERAL.emptyStateTitle) .hasText('Version 2 of this secret has been deleted', 'Shows deleted message'); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 deleted'); await assertVersionDropdown(assert, [2]); // important to test dropdown versions are accurate @@ -676,7 +668,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) // back to secret tab to confirm deleted state await click(PAGE.secretTab('Secret')); // if this assertion fails, the view is rendering a stale model - assert.dom(PAGE.emptyStateTitle).exists('still renders empty state!!'); + assert.dom(GENERAL.emptyStateTitle).exists('still renders empty state!!'); await assertVersionDropdown(assert, [2]); // undelete flow @@ -696,7 +688,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) await click(PAGE.secretTab('Secret')); assertDeleteActions(assert, []); assert - .dom(PAGE.emptyStateTitle) + .dom(GENERAL.emptyStateTitle) .hasText('Version 2 of this secret has been permanently destroyed', 'Shows destroyed message'); // navigate to sibling route to make sure empty state remains for details tab @@ -706,7 +698,7 @@ module('Acceptance | Enterprise | kv-v2 workflow | edge cases', function (hooks) // back to secret tab to confirm destroyed state await click(PAGE.secretTab('Secret')); // if this assertion fails, the view is rendering a stale model - assert.dom(PAGE.emptyStateTitle).exists('still renders empty state!!'); + assert.dom(GENERAL.emptyStateTitle).exists('still renders empty state!!'); await assertVersionDropdown(assert, [2]); }); }); diff --git a/ui/tests/acceptance/secrets/backend/kv/secret-test.js b/ui/tests/acceptance/secrets/backend/kv/secret-test.js index 992a477b9e..8ce35b7932 100644 --- a/ui/tests/acceptance/secrets/backend/kv/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/secret-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, visit, settled, currentURL, currentRouteName, fillIn } from '@ember/test-helpers'; +import { click, visit, settled, currentURL, currentRouteName, fillIn, waitFor } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -16,7 +16,7 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands'; import { runCmd } from 'vault/tests/helpers/commands'; import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { SECRET_ENGINE_SELECTORS as SS } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; @@ -50,11 +50,13 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { await mountSecrets.visit(); await click(MOUNT_BACKEND_FORM.mountType('kv')); await fillIn(GENERAL.inputByAttr('path'), enginePath); - await fillIn('[data-test-input="kv_config.max_versions"]', maxVersion); - await click('[data-test-input="kv_config.cas_required"]'); - await click('[data-test-toggle-label="Automate secret deletion"]'); - await fillIn('[data-test-select="ttl-unit"]', 's'); - await fillIn('[data-test-ttl-value="Automate secret deletion"]', '1'); + + await fillIn(GENERAL.inputByAttr('kv_config.max_versions'), maxVersion); + await click(GENERAL.inputByAttr('kv_config.cas_required')); + await click(GENERAL.ttl.toggle('Automate secret deletion')); + await fillIn(GENERAL.selectByAttr('ttl-unit'), 's'); + await fillIn(GENERAL.ttl.input('Automate secret deletion'), '1'); + await click(GENERAL.submitButton); await click(PAGE.secretTab('Configuration')); @@ -170,24 +172,24 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { // navigate to farthest leaf await visit(`/vault/secrets/${enginePath}/list`); assert.dom('[data-test-component="navigate-input"]').hasNoValue(); - assert.dom('[data-test-secret-link]').exists({ count: 1 }); - await click('[data-test-secret-link="1/"]'); + assert.dom(SS.secretLink()).exists({ count: 1 }); + await click(SS.secretLink('1/')); assert.dom('[data-test-component="navigate-input"]').hasValue('1/'); - assert.dom('[data-test-secret-link]').exists({ count: 2 }); - await click('[data-test-secret-link="1/2/"]'); + assert.dom(SS.secretLink()).exists({ count: 2 }); + await click(SS.secretLink('1/2/')); assert.dom('[data-test-component="navigate-input"]').hasValue('1/2/'); - assert.dom('[data-test-secret-link]').exists({ count: 1 }); - await click('[data-test-secret-link="1/2/3/"]'); + assert.dom(SS.secretLink()).exists({ count: 1 }); + await click(SS.secretLink('1/2/3/')); assert.dom('[data-test-component="navigate-input"]').hasValue('1/2/3/'); - assert.dom('[data-test-secret-link]').exists({ count: 2 }); + assert.dom(SS.secretLink()).exists({ count: 2 }); // delete the items await click(SS.secretLinkMenu('1/2/3/4')); - await click(`[data-test-secret-link="1/2/3/4"] ${GENERAL.confirmTrigger}`); + await click(`${SS.secretLink('1/2/3/4')} ${GENERAL.confirmTrigger}`); await click(GENERAL.confirmButton); assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.list'); assert.strictEqual(currentURL(), `/vault/secrets/${enginePath}/list/1/2/3/`, 'remains on the page'); - assert.dom('[data-test-secret-link]').exists({ count: 1 }); + assert.dom(SS.secretLink()).exists({ count: 1 }); await listPage.secrets.objectAt(0).menuToggle(); await click(GENERAL.confirmTrigger); @@ -200,7 +202,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { await click('[data-test-list-root-link]'); assert.strictEqual(currentURL(), `/vault/secrets/${enginePath}/list`); - assert.dom('[data-test-secret-link]').exists({ count: 1 }); + assert.dom(SS.secretLink()).exists({ count: 1 }); }); test('first level secrets redirect properly upon deletion', async function (assert) { @@ -267,16 +269,16 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { await listPage.visitRoot({ backend: enginePath }); await settled(); - assert.dom(`[data-test-secret-link="${firstPath}/"]`).exists('First section item exists'); - await click(`[data-test-secret-link="${firstPath}/"]`); + assert.dom(SS.secretLink(`${firstPath}/`)).exists('First section item exists'); + await click(SS.secretLink(`${firstPath}/`)); assert.strictEqual( currentURL(), `/vault/secrets/${enginePath}/list/${encodeURIComponent(firstPath)}/`, 'First part of path is encoded in URL' ); - assert.dom(`[data-test-secret-link="${secretPath}"]`).exists('Link to secret exists'); - await click(`[data-test-secret-link="${secretPath}"]`); + assert.dom(SS.secretLink(secretPath)).exists('Link to secret exists'); + await click(SS.secretLink(secretPath)); assert.strictEqual( currentURL(), `/vault/secrets/${enginePath}/show/${encodeURIComponent(firstPath)}/${encodeURIComponent( @@ -285,7 +287,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { 'secret path is encoded in URL' ); assert.dom('h1').hasText(secretPath, 'Path renders correctly on show page'); - await click(`[data-test-secret-breadcrumb="${firstPath}"] a`); + await click(SS.crumb(firstPath)); assert.strictEqual( currentURL(), `/vault/secrets/${enginePath}/list/${encodeURIComponent(firstPath)}/`, @@ -328,7 +330,7 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { await listPage.filterInput('filter/foo1'); assert.strictEqual(listPage.secrets.length, 1, 'renders only one secret'); await listPage.secrets.objectAt(0).click(); - await click('[data-test-secret-breadcrumb="filter"] a'); + await click(SS.crumb('filter')); assert.strictEqual(listPage.secrets.length, 3, 'renders three secrets'); assert.strictEqual(listPage.filterInputValue, 'filter/', 'pageFilter has been reset'); }); @@ -340,7 +342,11 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { await click(SS.createSecretLink); await fillIn(SS.secretPath('create'), secretPath); await click(GENERAL.toggleInput('json')); - codemirror().setValue(content); + + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, content); + await click(GENERAL.submitButton); assert.strictEqual( @@ -349,11 +355,12 @@ module('Acceptance | secrets/secret/create, read, delete', function (hooks) { 'redirects to the show page' ); assert.ok(showPage.editIsPresent, 'shows the edit button'); - assert.strictEqual( - codemirror().options.value, - JSON.stringify({ bar: 'boo', foo: 'fa' }, null, 2), - 'saves the content' - ); + assert + .dom('.hds-code-block') + .includesText( + `Secret Data ${JSON.stringify({ bar: 'boo', foo: 'fa' }, null, 2).replace(/\n\s*/g, ' ').trim()}`, + 'shows the secret data' + ); }); }); }); diff --git a/ui/tests/acceptance/tools-test.js b/ui/tests/acceptance/tools-test.js index 31047aacd5..00c4e6f55d 100644 --- a/ui/tests/acceptance/tools-test.js +++ b/ui/tests/acceptance/tools-test.js @@ -8,8 +8,10 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { toolsActions } from 'vault/helpers/tools-actions'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; + import { capitalize } from '@ember/string'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { assertCodeBlockValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; + import { setupMirage } from 'ember-cli-mirage/test-support'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors'; @@ -48,8 +50,10 @@ module('Acceptance | tools', function (hooks) { test('it wraps data, performs lookup, rewraps and then unwraps data', async function (assert) { const tokenStore = createTokenStore(); - await waitUntil(() => find('.CodeMirror')); - codemirror().setValue(DATA_TO_WRAP); + await waitUntil(() => find('.cm-editor')); + + const editor = codemirror(); + setCodeEditorValue(editor, DATA_TO_WRAP); await click(GENERAL.submitButton); const wrappedToken = await waitUntil(() => find(TS.toolsInput('wrapping-token'))); @@ -79,23 +83,16 @@ module('Acceptance | tools', function (hooks) { // unwrap await click(GENERAL.navLink('Unwrap')); - await fillIn(TS.toolsInput('unwrap-token'), tokenStore.get()); await click(GENERAL.submitButton); - await waitUntil(() => find('.CodeMirror')); - assert.deepEqual( - JSON.parse(codemirror().getValue()), - JSON.parse(DATA_TO_WRAP), - 'unwrapped data equals input data' - ); + + await waitUntil(() => find('.hds-code-block__code')); + assertCodeBlockValue(assert, '.hds-code-block__code', DATA_TO_WRAP); + await waitUntil(() => find(GENERAL.hdsTab('details'))); await click(GENERAL.hdsTab('details')); await click(GENERAL.hdsTab('data')); - assert.deepEqual( - JSON.parse(codemirror().getValue()), - JSON.parse(DATA_TO_WRAP), - 'data tab still has unwrapped data' - ); + assertCodeBlockValue(assert, '.hds-code-block__code', DATA_TO_WRAP); }); }); @@ -164,13 +161,9 @@ module('Acceptance | tools', function (hooks) { await fillIn(TS.toolsInput('unwrap-token'), 'sometoken'); await click(GENERAL.submitButton); - await waitUntil(() => find('.CodeMirror')); - assert.deepEqual( - AUTH_RESPONSE.auth, - JSON.parse(codemirror().getValue()), - 'unwrapped data equals input data' - ); + await waitUntil(() => find('.hds-code-block__code')); + assertCodeBlockValue(assert, '.hds-code-block__code', AUTH_RESPONSE.auth); }); }); @@ -179,8 +172,9 @@ module('Acceptance | tools', function (hooks) { const tokenStore = createTokenStore(); await visit('/vault/tools/wrap'); - await waitUntil(() => find('.CodeMirror')); - codemirror().setValue(DATA_TO_WRAP); + await waitUntil(() => find('.cm-editor')); + const editor = codemirror(); + setCodeEditorValue(editor, DATA_TO_WRAP); // initial wrap await click(GENERAL.submitButton); @@ -199,16 +193,18 @@ module('Acceptance | tools', function (hooks) { await click(GENERAL.navLink('Unwrap')); await fillIn(TS.toolsInput('unwrap-token'), tokenStore.get()); await click(GENERAL.submitButton); - await waitUntil(() => find('.CodeMirror')); - assert.strictEqual(codemirror().getValue(' '), '{ "tools": "tests" }', 'it renders unwrapped data'); + + await waitUntil(() => find('.hds-code-block__code')); + assertCodeBlockValue(assert, '.hds-code-block__code', '{ "tools": "tests" }'); }); test('it sends wrap ttl', async function (assert) { const tokenStore = createTokenStore(); await visit('/vault/tools/wrap'); - await waitUntil(() => find('.CodeMirror')); - codemirror().setValue(DATA_TO_WRAP); + await waitUntil(() => find('.cm-editor')); + const editor = codemirror(); + setCodeEditorValue(editor, DATA_TO_WRAP); // update to non-default ttl await click(GENERAL.toggleInput('Wrap TTL')); diff --git a/ui/tests/acceptance/transit-test.js b/ui/tests/acceptance/transit-test.js index b9213e9494..425e2efbe7 100644 --- a/ui/tests/acceptance/transit-test.js +++ b/ui/tests/acceptance/transit-test.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click, fillIn, find, currentURL, settled, visit, findAll } from '@ember/test-helpers'; +import { click, fillIn, find, currentURL, settled, visit, findAll, waitFor } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { v4 as uuidv4 } from 'uuid'; @@ -11,7 +11,7 @@ import { v4 as uuidv4 } from 'uuid'; import { encodeString } from 'vault/utils/b64'; import { login } from 'vault/tests/helpers/auth/auth-helpers'; import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors'; @@ -150,7 +150,9 @@ const testConvergentEncryption = async function (assert, keyName) { for (const testCase of tests) { await click('[data-test-transit-action-link="encrypt"]'); - codemirror('#plaintext-control').setValue(testCase.plaintext); + await waitFor('.cm-editor'); + const editor = codemirror('#plaintext-control'); + setCodeEditorValue(editor, testCase.plaintext); await fillIn('[data-test-transit-input="context"]', testCase.context); if (!testCase.encodePlaintext) { @@ -179,7 +181,8 @@ const testConvergentEncryption = async function (assert, keyName) { testCase.assertBeforeDecrypt(keyName); } - codemirror('#ciphertext-control').setValue(copiedCiphertext); + setCodeEditorValue(editor, copiedCiphertext); + await click(GENERAL.submitButton); if (testCase.assertAfterDecrypt) { @@ -235,7 +238,7 @@ module('Acceptance | transit', function (hooks) { await click(SELECTORS.form('exportable')); await click(SELECTORS.form('derived')); await click(SELECTORS.form('convergent-encryption')); - await click('[data-test-toggle-label="Auto-rotation period"]'); + await click(GENERAL.ttl.toggle('Auto-rotation period')); await click(SELECTORS.form('create')); assert.strictEqual( @@ -463,7 +466,7 @@ module('Acceptance | transit', function (hooks) { const expectedRotateValue = key.autoRotate ? '30 days' : 'Key will not be automatically rotated'; assert - .dom('[data-test-row-value="Auto-rotation period"]') + .dom(GENERAL.infoRowValue('Auto-rotation period')) .hasText(expectedRotateValue, 'Has expected auto rotate value'); await click(SELECTORS.versionsTab); diff --git a/ui/tests/helpers/codemirror.js b/ui/tests/helpers/codemirror.js index aba7e89189..71e7a5722f 100644 --- a/ui/tests/helpers/codemirror.js +++ b/ui/tests/helpers/codemirror.js @@ -18,17 +18,76 @@ sample use: assert.strictEqual(codemirror('#my-control').getValue(), 'some value') )} */ -export default function (parent) { - const selector = parent ? `${parent} .CodeMirror` : '.CodeMirror'; - const element = document.querySelector(selector); - invariant(element, `Selector '.CodeMirror' matched no elements`); - const cm = element.CodeMirror; - invariant(cm, `No registered CodeMirror instance for '.CodeMirror'`); +import { find } from '@ember/test-helpers'; + +export default function (parent) { + const selector = parent ? `${parent} .hds-code-editor__editor` : '.hds-code-editor__editor'; + const element = document.querySelector(selector); + invariant(element, `Selector '${selector}' matched no elements`); + + const cm = element.editor; + invariant(cm, `No registered CodeMirror instance for ''${selector}'`); return cm; } +export function setCodeEditorValue(editorView, value, { from, to } = {}) { + invariant(editorView, 'No editor view provided'); + invariant(value != null, 'No value provided'); + + editorView.dispatch({ + changes: [ + { + from: from ?? 0, + to: to ?? editorView.state.doc.length, + insert: value, + }, + ], + }); +} + +export function getCodeEditorValue(editorView) { + invariant(editorView, 'No editor view provided'); + + return editorView.state.doc.toString(); +} + +export function assertCodeBlockValue(assert, selector, expected) { + invariant(selector, 'No selector provided to assertCodeBlockValue'); + invariant( + typeof expected === 'string' || typeof expected === 'object', + 'Expected value must be a JSON string or an object' + ); + + const element = find(selector); + assert.ok(element, `Element "${selector}" should exist`); + + const raw = element.textContent.trim(); + assert.ok(raw.length > 0, `Element "${selector}" should not be empty`); + + let actual; + try { + actual = JSON.parse(raw); + } catch (err) { + throw new Error(`Could not parse JSON from "${selector}":\n${raw}`); + } + + // normalize the expected value into an object + const expectedObj = + typeof expected === 'string' + ? (() => { + try { + return JSON.parse(expected); + } catch { + throw new Error(`Expected string was not valid JSON:\n${expected}`); + } + })() + : expected; + + assert.deepEqual(actual, expectedObj); +} + const invariant = (truthy, error) => { if (!truthy) throw new Error(error); }; diff --git a/ui/tests/integration/components/console/log-json-test.js b/ui/tests/integration/components/console/log-json-test.js index 2552113608..5a3f86fb7f 100644 --- a/ui/tests/integration/components/console/log-json-test.js +++ b/ui/tests/integration/components/console/log-json-test.js @@ -5,16 +5,13 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, find } from '@ember/test-helpers'; +import { render } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; +import { assertCodeBlockValue } from 'vault/tests/helpers/codemirror'; module('Integration | Component | console/log json', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.codeMirror = this.owner.lookup('service:code-mirror'); - }); - test('it renders', async function (assert) { // Set any properties with this.set('myProperty', 'value'); // Handle any actions with this.on('myAction', function(val) { ... }); @@ -24,7 +21,6 @@ module('Integration | Component | console/log json', function (hooks) { this.set('content', objectContent); await render(hbs``); - const instance = find('[data-test-component=code-mirror-modifier]').innerText; - assert.strictEqual(instance, expectedText); + assertCodeBlockValue(assert, '.hds-code-block__code', expectedText); }); }); diff --git a/ui/tests/integration/components/json-editor-test.js b/ui/tests/integration/components/json-editor-test.js index c63e6fe0e9..17040dc106 100644 --- a/ui/tests/integration/components/json-editor-test.js +++ b/ui/tests/integration/components/json-editor-test.js @@ -5,16 +5,13 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import { render, fillIn, find, waitUntil, click, triggerKeyEvent } from '@ember/test-helpers'; +import { render, click, triggerKeyEvent, waitFor } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; -import jsonEditor from '../../pages/components/json-editor'; +import { SELECTORS } from '../../pages/components/json-editor'; import sinon from 'sinon'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { createLongJson } from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; -import { GENERAL } from 'vault/tests/helpers/general-selectors'; - -const component = create(jsonEditor); +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; module('Integration | Component | json-editor', function (hooks) { setupRenderingTest(hooks); @@ -53,48 +50,10 @@ module('Integration | Component | json-editor', function (hooks) { @readOnly={{true}} />`); - assert.strictEqual(component.title, 'Test title', 'renders the provided title'); - assert.true(component.hasToolbar, 'renders the toolbar'); - assert.dom(GENERAL.copyButton).exists('renders the copy button'); - assert.true(component.hasJSONEditor, 'renders the code mirror modifier'); - assert.ok(component.canEdit, 'json editor can be edited'); - }); - - test('it handles editing and linting and styles to json', async function (assert) { - await render(hbs``); - // check for json styling - assert.dom('.cm-property').hasStyle({ - color: 'rgb(158, 132, 197)', - }); - assert.dom('.cm-string:nth-child(2)').hasStyle({ - color: 'rgb(29, 219, 163)', - }); - - await fillIn('textarea', this.bad_json_blob); - await waitUntil(() => find('.CodeMirror-lint-marker-error')); - assert.dom('.CodeMirror-lint-marker-error').exists('throws linting error'); - assert.dom('.CodeMirror-linenumber').exists('shows line numbers'); - }); - - test('it renders the correct theme and expected styling', async function (assert) { - await render(hbs``); - // computed style differs between browsers so we only check for the color - // TODO: this component will be replaced with a hds codeEditor and this test can be removed - // getComputedStyle for Chrome: rgb(247, 248, 250) none repeat scroll 0% 0% / auto padding-box border-box - // getComputedStyle for Firefox: rgb(247, 248, 250); - assert.dom('.cm-s-hashi-read-only').hasStyle({ - backgroundColor: 'rgb(247, 248, 250)', - }); - assert.dom('.CodeMirror-linenumber').doesNotExist('on readOnly does not show line numbers'); + assert.dom(SELECTORS.title).hasText('Test title', 'renders the provided title'); + assert.dom(SELECTORS.toolbar).exists('renders the toolbar'); + assert.dom(SELECTORS.copy).exists('renders the copy button'); + assert.dom(SELECTORS.codeBlock).exists('renders the code block'); }); test('it should render example and restore it', async function (assert) { @@ -110,12 +69,22 @@ module('Integration | Component | json-editor', function (hooks) { /> `); - assert.dom('.CodeMirror-code').hasText(`1${this.example}`, 'Example renders when there is no value'); + let view = codemirror(); + await waitFor('.cm-editor'); + + let editorValue = getCodeEditorValue(view); + assert.strictEqual(editorValue, this.example, 'Example renders when there is no value'); assert.dom('[data-test-restore-example]').isDisabled('Restore button disabled when showing example'); - await fillIn('textarea', ''); - await fillIn('textarea', 'adding a value should allow the example to be restored'); + + setCodeEditorValue(view, ''); + setCodeEditorValue(view, 'adding a value should allow the example to be restored'); await click('[data-test-restore-example]'); - assert.dom('.CodeMirror-code').hasText(`1${this.example}`, 'Example is restored'); + + view = codemirror(); + await waitFor('.cm-editor'); + editorValue = getCodeEditorValue(view); + + assert.deepEqual(editorValue, this.example, 'Example is restored'); assert.strictEqual(this.value, null, 'Value is cleared on restore example'); }); @@ -129,40 +98,17 @@ module('Integration | Component | json-editor', function (hooks) { @valueUpdated={{fn (mut this.value)}} /> `); - await fillIn('textarea', '#A comment'); + + await waitFor('.cm-editor'); + const view = codemirror(); + setCodeEditorValue(view, '#A comment'); assert.strictEqual(this.value, '#A comment', 'value is set correctly'); - await triggerKeyEvent('textarea', 'keydown', 'Enter'); + + await triggerKeyEvent('.cm-content', 'keydown', 'Enter'); assert.strictEqual( this.value, - `#A comment\n`, + `\n#A comment`, 'even after hitting enter the value is still set correctly' ); }); - - test('no viewportMargin renders only default 10 lines of data on the DOM', async function (assert) { - await render(hbs` - - `); - assert - .dom('.CodeMirror-code') - .doesNotIncludeText('key-9', 'Without viewportMargin, user cannot search for key-9'); - }); - - test('when viewportMargin is set user is able to search a long secret', async function (assert) { - await render(hbs` - - `); - assert - .dom('.CodeMirror-code') - .containsText('key-9', 'With viewportMargin set, user can search for key-9'); - }); }); diff --git a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js index 5db6ee649f..273053455a 100644 --- a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js +++ b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js @@ -7,11 +7,12 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; -import { render, click, fillIn } from '@ember/test-helpers'; +import { render, click, fillIn, waitFor, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', function (hooks) { setupRenderingTest(hooks); @@ -58,16 +59,14 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct hbs``, { owner: this.engine } ); + assert.dom(GENERAL.emptyStateTitle).hasText('Choose an option above', 'Empty state title renders'); assert - .dom('[data-test-empty-state-title]') - .hasText('Choose an option above', 'Empty state title renders'); - assert - .dom('[data-test-empty-state-message]') + .dom(GENERAL.emptyStateMessage) .hasText( 'To configure a Vault role, choose what should be generated in Kubernetes by Vault.', 'Empty state message renders' ); - assert.dom('[data-test-submit]').isDisabled('Save button is disabled'); + assert.dom(GENERAL.submitButton).isDisabled('Save button is disabled'); }); test('it should display different form fields based on generation preference selection', async function (assert) { @@ -85,17 +84,17 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click('[data-test-radio-card="basic"]'); ['serviceAccountName', ...commonFields].forEach((field) => { - assert.dom(`[data-test-field="${field}"]`).exists(`${field} field renders`); + assert.dom(GENERAL.fieldByAttr(field)).exists(`${field} field renders`); }); await click('[data-test-radio-card="expanded"]'); ['kubernetesRoleType', 'kubernetesRoleName', 'nameTemplate', ...commonFields].forEach((field) => { - assert.dom(`[data-test-field="${field}"]`).exists(`${field} field renders`); + assert.dom(GENERAL.fieldByAttr(field)).exists(`${field} field renders`); }); await click('[data-test-radio-card="full"]'); ['kubernetesRoleType', 'nameTemplate', ...commonFields].forEach((field) => { - assert.dom(`[data-test-field="${field}"]`).exists(`${field} field renders`); + assert.dom(GENERAL.fieldByAttr(field)).exists(`${field} field renders`); }); assert.dom('[data-test-generated-role-rules]').exists('Generated role rules section renders'); }); @@ -107,7 +106,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct ); await click('[data-test-radio-card="basic"]'); - await fillIn('[data-test-input="serviceAccountName"]', 'test'); + await fillIn(GENERAL.inputByAttr('serviceAccountName'), 'test'); await click('[data-test-radio-card="expanded"]'); assert.strictEqual( this.newModel.serviceAccountName, @@ -115,7 +114,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct 'Service account name cleared when switching from basic to expanded' ); - await fillIn('[data-test-input="kubernetesRoleName"]', 'test'); + await fillIn(GENERAL.inputByAttr('kubernetesRoleName'), 'test'); await click('[data-test-radio-card="full"]'); assert.strictEqual( this.newModel.kubernetesRoleName, @@ -123,9 +122,9 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct 'Kubernetes role name cleared when switching from expanded to full' ); - await click('[data-test-input-group="kubernetesRoleType"] input'); + await click(`${GENERAL.inputGroupByAttr('kubernetesRoleType')} input`); await click(GENERAL.toggleInput('show-nameTemplate')); - await fillIn('[data-test-input="nameTemplate"]', 'bar'); + await fillIn(GENERAL.inputByAttr('nameTemplate'), 'bar'); await fillIn('[data-test-select-template]', '6'); await click('[data-test-radio-card="expanded"]'); assert.strictEqual( @@ -162,11 +161,13 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct { owner: this.engine } ); await click('[data-test-radio-card="basic"]'); - await click('[data-test-submit]'); + + await click(GENERAL.submitButton); 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]'); + await fillIn(GENERAL.inputByAttr('name'), 'role-1'); + await fillIn(GENERAL.inputByAttr('serviceAccountName'), 'default'); + await click(GENERAL.submitButton); + assert.ok( this.transitionCalledWith('roles.role.details', this.newModel.name), 'Transitions to details route on save' @@ -186,11 +187,11 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct { owner: this.engine } ); assert.dom(`[data-test-radio-card="${pref}"] input`).isChecked('Correct radio card is checked'); - assert.dom('[data-test-input="name"]').hasValue(this.role.name, 'Role name is populated'); + assert.dom(GENERAL.inputByAttr('name')).hasValue(this.role.name, 'Role name is populated'); const selector = { - basic: { name: '[data-test-input="serviceAccountName"]', method: 'hasValue', value: 'default' }, + basic: { name: GENERAL.inputByAttr('serviceAccountName'), method: 'hasValue', value: 'default' }, expanded: { - name: '[data-test-input="kubernetesRoleName"]', + name: GENERAL.inputByAttr('kubernetesRoleName'), method: 'hasValue', value: 'vault-k8s-secrets-role', }, @@ -201,7 +202,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct }, }[pref]; assert.dom(selector.name)[selector.method](selector.value); - await click('[data-test-submit]'); + await click(GENERAL.submitButton); assert.ok( this.transitionCalledWith('roles.role.details', this.role.name), 'Transitions to details route on save' @@ -217,15 +218,15 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await click('[data-test-radio-card="basic"]'); assert.dom('[data-test-annotations]').doesNotExist('Annotations and labels are hidden'); - await click('[data-test-field="annotations"]'); + await click(GENERAL.fieldByAttr('annotations')); await fillIn('[data-test-kv="annotations"] [data-test-kv-key]', 'foo'); await fillIn('[data-test-kv="annotations"] [data-test-kv-value]', 'bar'); - await click('[data-test-kv="annotations"] [data-test-kv-add-row]'); + await click(`[data-test-kv="annotations"] ${GENERAL.kvObjectEditor.addRow}`); assert.deepEqual(this.newModel.extraAnnotations, { foo: 'bar' }, 'Annotations set'); await fillIn('[data-test-kv="labels"] [data-test-kv-key]', 'bar'); await fillIn('[data-test-kv="labels"] [data-test-kv-value]', 'baz'); - await click('[data-test-kv="labels"] [data-test-kv-add-row]'); + await click(`[data-test-kv="labels"] ${GENERAL.kvObjectEditor.addRow}`); assert.deepEqual(this.newModel.extraLabels, { bar: 'baz' }, 'Labels set'); }); @@ -252,9 +253,12 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct owner: this.engine, }); const addedText = 'this will be add to the start of the first line in the JsonEditor'; - await fillIn('[data-test-component="code-mirror-modifier"] textarea', addedText); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, addedText); + await settled(); await click('[data-test-restore-example]'); - assert.dom('.CodeMirror-code').doesNotContainText(addedText, 'Role rules example restored'); + assert.dom('.cm-content').doesNotContainText(addedText, 'Role rules example restored'); }); test('it should set generatedRoleRoles model prop on save', async function (assert) { @@ -275,9 +279,9 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct { owner: this.engine } ); await click('[data-test-radio-card="full"]'); - await fillIn('[data-test-input="name"]', 'role-1'); + await fillIn(GENERAL.inputByAttr('name'), 'role-1'); await fillIn('[data-test-select-template]', '5'); - await click('[data-test-submit]'); + await click(GENERAL.submitButton); }); test('it should unset selectedTemplateId when switching from full generation preference', async function (assert) { @@ -293,11 +297,11 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct { owner: this.engine } ); await click('[data-test-radio-card="full"]'); - await fillIn('[data-test-input="name"]', 'role-1'); + await fillIn(GENERAL.inputByAttr('name'), 'role-1'); await fillIn('[data-test-select-template]', '5'); await click('[data-test-radio-card="basic"]'); - await fillIn('[data-test-input="serviceAccountName"]', 'default'); - await click('[data-test-submit]'); + await fillIn(GENERAL.inputByAttr('serviceAccountName'), 'default'); + await click(GENERAL.submitButton); }); test('it should go back to list route and clean up model', async function (assert) { @@ -306,7 +310,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct hbs``, { owner: this.engine } ); - await click('[data-test-cancel]'); + await click(GENERAL.cancelButton); assert.ok(unloadSpy.calledOnce, 'New model is unloaded on cancel'); assert.ok(this.transitionCalledWith('roles'), 'Transitions to roles list on cancel'); @@ -315,7 +319,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct await render(hbs``, { owner: this.engine, }); - await click('[data-test-cancel]'); + await click(GENERAL.cancelButton); assert.ok(rollbackSpy.calledOnce, 'Attributes are rolled back for existing model on cancel'); assert.ok(this.transitionCalledWith('roles'), 'Transitions to roles list on cancel'); }); @@ -326,10 +330,12 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct { owner: this.engine } ); await click('[data-test-radio-card="basic"]'); - await click('[data-test-submit]'); + + await click(GENERAL.submitButton); assert.dom(GENERAL.validationErrorByAttr('name')).hasText('Name is required'); + assert - .dom('[data-test-invalid-form-alert] [data-test-inline-error-message]') + .dom(`[data-test-invalid-form-alert] ${GENERAL.inlineError}`) .hasText('There is an error with this form.'); }); @@ -355,7 +361,7 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct owner: this.engine, }); - await fillIn('[data-test-input="serviceAccountName"]', 'demo'); - await click('[data-test-submit]'); + await fillIn(GENERAL.inputByAttr('serviceAccountName'), 'demo'); + await click(GENERAL.submitButton); }); }); diff --git a/ui/tests/integration/components/kv/kv-data-fields-test.js b/ui/tests/integration/components/kv/kv-data-fields-test.js index ac3d5ed8b6..84bfead9dd 100644 --- a/ui/tests/integration/components/kv/kv-data-fields-test.js +++ b/ui/tests/integration/components/kv/kv-data-fields-test.js @@ -8,12 +8,11 @@ import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { hbs } from 'ember-cli-htmlbars'; -import { fillIn, render, click } from '@ember/test-helpers'; -import codemirror from 'vault/tests/helpers/codemirror'; +import { fillIn, render, click, waitFor, findAll } from '@ember/test-helpers'; +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { PAGE, FORM } from 'vault/tests/helpers/kv/kv-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { setRunOptions } from 'ember-a11y-testing/test-support'; -import { createLongJson } from 'vault/tests/helpers/secret-engine/secret-engine-helpers'; module('Integration | Component | kv-v2 | KvDataFields', function (hooks) { setupRenderingTest(hooks); @@ -50,14 +49,22 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) { assert.expect(3); await render(hbs``, { owner: this.engine }); + await waitFor('.cm-editor'); + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); assert.strictEqual( - codemirror().getValue(' '), - `{ \"\": \"\" }`, // eslint-disable-line no-useless-escape + editorValue, + `{ + "": "" +}`, 'json editor initializes with empty object that includes whitespace' ); - await fillIn(`${FORM.jsonEditor} textarea`, 'blah'); - assert.strictEqual(codemirror().state.lint.marked.length, 1, 'codemirror lints input'); - codemirror().setValue(`{ "hello": "there"}`); + setCodeEditorValue(editor, 'blah'); + + await waitFor('.cm-lint-marker'); + const lintMarkers = findAll('.cm-lint-marker'); + assert.strictEqual(lintMarkers.length, 1, 'codemirror lints input'); + setCodeEditorValue(editor, `{ "hello": "there"}`); assert.propEqual(this.secret.secretData, { hello: 'there' }, 'json editor updates secret data'); }); @@ -114,19 +121,4 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) { .dom(GENERAL.codeBlock('secret-data')) .hasText(`Version data { "foo": { "bar": "baz" } } `, 'Json data is displayed'); }); - - test('it defaults to a viewportMargin 10 when there is no secret data', async function (assert) { - await render(hbs``, { owner: this.engine }); - assert.strictEqual(codemirror().options.viewportMargin, 10, 'viewportMargin defaults to 10'); - }); - - test('it calculates viewportMargin based on secret size', async function (assert) { - this.secret.secretData = createLongJson(100); - await render(hbs``, { owner: this.engine }); - assert.strictEqual( - codemirror().options.viewportMargin, - 100, - 'viewportMargin is set to 100 matching the height of the json' - ); - }); }); diff --git a/ui/tests/integration/components/kv/kv-patch/json-form-test.js b/ui/tests/integration/components/kv/kv-patch/json-form-test.js index 37703e2bd9..b1dc1d237f 100644 --- a/ui/tests/integration/components/kv/kv-patch/json-form-test.js +++ b/ui/tests/integration/components/kv/kv-patch/json-form-test.js @@ -6,12 +6,12 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; import { setupEngine } from 'ember-engines/test-support'; -import { click, render } from '@ember/test-helpers'; +import { click, render, waitFor, settled } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import sinon from 'sinon'; -import { FORM, parseObject } from 'vault/tests/helpers/kv/kv-selectors'; -import codemirror from 'vault/tests/helpers/codemirror'; +import { FORM } from 'vault/tests/helpers/kv/kv-selectors'; +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; module('Integration | Component | kv | kv-patch/editor/json-form', function (hooks) { setupRenderingTest(hooks); @@ -32,7 +32,7 @@ module('Integration | Component | kv | kv-patch/editor/json-form', function (hoo }, }; this.renderComponent = async () => { - return render( + await render( hbs` `, { owner: this.engine } ); + return waitFor('.cm-editor'); }; }); test('it renders', async function (assert) { await this.renderComponent(); - assert.propEqual(parseObject(codemirror), { '': '' }, 'json editor initializes with empty object'); + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); + assert.strictEqual( + editorValue, + `{ + "": "" +}`, + 'json editor initializes with empty object' + ); await click(FORM.saveBtn); assert.true(this.onSubmit.calledOnce, 'clicking "Save" calls @onSubmit'); await click(FORM.cancelBtn); @@ -71,11 +80,14 @@ module('Integration | Component | kv | kv-patch/editor/json-form', function (hoo test('it renders linting errors', async function (assert) { await this.renderComponent(); - await codemirror().setValue('{ "foo3": }'); + const editor = codemirror(); + setCodeEditorValue(editor, '{ "foo3": }'); + await settled(); assert .dom(GENERAL.inlineError) .hasText('JSON is unparsable. Fix linting errors to avoid data discrepancies.'); - await codemirror().setValue('{ "foo": "bar" }'); + setCodeEditorValue(editor, '{ "foo": "bar" }'); + await settled(); assert.dom(GENERAL.inlineError).doesNotExist('error disappears when linting is fixed'); }); @@ -88,7 +100,8 @@ module('Integration | Component | kv | kv-patch/editor/json-form', function (hoo test('it submits data', async function (assert) { this.submitError = 'There was a problem'; await this.renderComponent(); - await codemirror().setValue('{ "foo": "bar" }'); + const editor = codemirror(); + setCodeEditorValue(editor, '{ "foo": "bar" }'); await click(FORM.saveBtn); const [data] = this.onSubmit.lastCall.args; assert.propEqual(data, { foo: 'bar' }, `onSubmit called with ${JSON.stringify(data)}`); diff --git a/ui/tests/integration/components/kv/page/kv-page-patch-test.js b/ui/tests/integration/components/kv/page/kv-page-patch-test.js index fc473d2f4f..5aa5af0ccf 100644 --- a/ui/tests/integration/components/kv/page/kv-page-patch-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-patch-test.js @@ -13,7 +13,7 @@ import sinon from 'sinon'; import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { encodePath } from 'vault/utils/path-encoding-helpers'; import { overrideResponse } from 'vault/tests/helpers/stubs'; @@ -175,8 +175,9 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks) }); await this.renderComponent(); await click(GENERAL.inputByAttr('JSON')); - await waitUntil(() => find('.CodeMirror')); - await codemirror().setValue('{ "foo": "foovalue", "bar":null, "number":1 }'); + await waitUntil(() => find('.cm-editor')); + const editor = codemirror(); + setCodeEditorValue(editor, '{ "foo": "foovalue", "bar":null, "number":1 }'); await click(FORM.saveBtn); const [route] = this.transitionStub.lastCall.args; assert.strictEqual( @@ -240,8 +241,9 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks) await this.renderComponent(); await click(GENERAL.inputByAttr('JSON')); - await waitUntil(() => find('.CodeMirror')); - await codemirror().setValue('{ "foo": "" }'); + await waitUntil(() => find('.cm-editor')); + const editor = codemirror(); + setCodeEditorValue(editor, '{ "foo": "" }'); await click(FORM.saveBtn); }); @@ -313,7 +315,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks) ); await this.renderComponent(); await click(GENERAL.inputByAttr('JSON')); - await waitUntil(() => find('.CodeMirror')); + await waitUntil(() => find('.cm-editor')); await click(FORM.saveBtn); assert.dom(GENERAL.messageError).doesNotExist('PATCH request is not made'); const route = this.transitionStub.lastCall?.args[0] || ''; @@ -363,8 +365,9 @@ module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks) assert.expect(2); await this.renderComponent(); await click(GENERAL.inputByAttr('JSON')); - await waitUntil(() => find('.CodeMirror')); - await codemirror().setValue('{ "foo": "foovalue", "bar":null, "number":1 }'); + await waitUntil(() => find('.cm-editor')); + const editor = codemirror(); + setCodeEditorValue(editor, '{ "foo": "foovalue", "bar":null, "number":1 }'); await click(FORM.saveBtn); await click(FORM.saveBtn); assert.dom(GENERAL.messageError).hasText('Error permission denied'); diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js index 8ed847c1ba..16d02831db 100644 --- a/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js @@ -9,8 +9,8 @@ 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, render } from '@ember/test-helpers'; -import codemirror from 'vault/tests/helpers/codemirror'; +import { click, fillIn, render, settled, waitFor } from '@ember/test-helpers'; +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors'; import sinon from 'sinon'; import { setRunOptions } from 'ember-a11y-testing/test-support'; @@ -89,9 +89,14 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks) assert.dom(FORM.maskedValueInput()).hasValue('bar'); assert.dom(FORM.dataInputLabel({ isJson: false })).hasText('Version data'); await click(GENERAL.toggleInput('json')); + await waitFor('.cm-editor'); + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); assert.strictEqual( - codemirror().getValue(' '), - `{ \"foo": \"bar" }`, // eslint-disable-line no-useless-escape + editorValue, + `{ + "foo": "bar" +}`, 'json editor initializes with empty object' ); assert.dom(FORM.dataInputLabel({ isJson: true })).hasText('Version data'); @@ -133,7 +138,11 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks) assert.dom(PAGE.diff.added).hasText(`foo2"bar2"`); await click(GENERAL.toggleInput('json')); - codemirror().setValue('{ "foo3": "bar3" }'); + await waitFor('.cm-editor'); + const editor = codemirror(); + + setCodeEditorValue(editor, '{ "foo3": "bar3" }'); + await settled(); assert.dom(PAGE.diff.visualDiff).exists('Visual diff updates'); assert.dom(PAGE.diff.deleted).hasText(`foo"bar"`); @@ -219,12 +228,16 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks) ); await click(GENERAL.toggleInput('json')); - codemirror().setValue('i am a string and not JSON'); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, 'i am a string and not JSON'); + await settled(); assert .dom(FORM.inlineAlert) .hasText('JSON is unparsable. Fix linting errors to avoid data discrepancies.'); - codemirror().setValue(`""`); + setCodeEditorValue(editor, '""'); + await settled(); await click(FORM.saveBtn); assert.dom(FORM.inlineAlert).hasText('Vault expects data to be formatted as an JSON object.'); }); @@ -263,8 +276,9 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks) assert.dom(FORM.dataInputLabel({ isJson: false })).hasText('Version data'); await click(GENERAL.toggleInput('json')); assert.dom(FORM.dataInputLabel({ isJson: true })).hasText('Version data'); - - codemirror().setValue(`{ "hello": "there"}`); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, '{ "hello": "there" }'); await click(FORM.saveBtn); }); diff --git a/ui/tests/integration/components/kv/page/kv-page-secrets-create-test.js b/ui/tests/integration/components/kv/page/kv-page-secrets-create-test.js index 06fdd036bc..e7be657e01 100644 --- a/ui/tests/integration/components/kv/page/kv-page-secrets-create-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-secrets-create-test.js @@ -9,8 +9,10 @@ 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, render, typeIn } from '@ember/test-helpers'; -import codemirror from 'vault/tests/helpers/codemirror'; + +import { click, fillIn, render, typeIn, waitFor, settled } from '@ember/test-helpers'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; + import { FORM } from 'vault/tests/helpers/kv/kv-selectors'; import sinon from 'sinon'; import { setRunOptions } from 'ember-a11y-testing/test-support'; @@ -96,8 +98,8 @@ module('Integration | Component | kv-v2 | Page::Secrets::Create', function (hook await fillIn(FORM.maskedValueInput(), 'bar'); await click(FORM.toggleMetadata); - await fillIn(`[data-test-field="customMetadata"] ${FORM.keyInput()}`, 'my-custom'); - await fillIn(`[data-test-field="customMetadata"] ${FORM.valueInput()}`, 'metadata'); + await fillIn(`${GENERAL.fieldByAttr('customMetadata')} ${FORM.keyInput()}`, 'my-custom'); + await fillIn(`${GENERAL.fieldByAttr('customMetadata')} ${FORM.valueInput()}`, 'metadata'); await fillIn(FORM.inputByAttr('maxVersions'), this.maxVersions); await click(FORM.saveBtn); @@ -250,12 +252,16 @@ module('Integration | Component | kv-v2 | Page::Secrets::Create', function (hook .doesNotExist('it removes validation on key up when secret contains slash but does not end in one'); await click(GENERAL.toggleInput('json')); - codemirror().setValue('i am a string and not JSON'); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, 'i am a string and not JSON'); + await settled(); assert .dom(FORM.inlineAlert) .hasText('JSON is unparsable. Fix linting errors to avoid data discrepancies.'); - codemirror().setValue('{}'); // clear linting error + setCodeEditorValue(editor, '{}'); + await settled(); await fillIn(FORM.inputByAttr('path'), ''); await click(FORM.saveBtn); assert.dom(FORM.validationError('path')).hasText(`Path can't be blank.`); @@ -296,7 +302,9 @@ module('Integration | Component | kv-v2 | Page::Secrets::Create', function (hook await click(GENERAL.toggleInput('json')); assert.dom(FORM.dataInputLabel({ isJson: true })).hasText('Secret data'); - codemirror().setValue(`{ "hello": "there"}`); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, `{ "hello": "there"}`); await fillIn(FORM.inputByAttr('path'), this.path); await click(FORM.saveBtn); }); diff --git a/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js b/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js index 566c1474f7..183811f913 100644 --- a/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js +++ b/ui/tests/integration/components/ldap/page/role/create-and-edit-test.js @@ -67,9 +67,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h const checkFields = (fields) => { fields.forEach((field) => { - assert - .dom(`[data-test-field="${field}"]`) - .exists(`${field} field renders when static type is selected`); + assert.dom(GENERAL.fieldByAttr(field)).exists(`${field} field renders when static type is selected`); }); }; @@ -94,13 +92,13 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h const isLdif = field.includes('ldif'); const method = isLdif ? 'includesText' : 'hasValue'; const value = isLdif ? 'dn: cn={{.Username}},ou=users,dc=learn,dc=example' : this.model[field]; - assert.dom(`[data-test-field="${field}"] ${element}`)[method](value, `${field} field value renders`); + assert.dom(`${GENERAL.fieldByAttr(field)} ${element}`)[method](value, `${field} field value renders`); }); }; const checkTtl = (fields) => { fields.forEach((field) => { assert - .dom(`[data-test-field="${field}"] [data-test-ttl-inputs] input`) + .dom(`${GENERAL.fieldByAttr(field)} [data-test-ttl-inputs] input`) .hasAnyValue(`${field} field ttl value renders`); }); }; @@ -108,7 +106,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h this.model = this.fetchModel('static', 'static-role'); await this.renderComponent(); assert.dom('[data-test-radio-card="static"]').isDisabled('Type selection is disabled when editing'); - assert.dom('[data-test-input="name"]').isDisabled('Name field is disabled when editing'); + assert.dom(GENERAL.inputByAttr('name')).isDisabled('Name field is disabled when editing'); checkFields(['name', 'dn', 'username']); checkTtl(['rotation_period']); @@ -116,7 +114,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h await this.renderComponent(); checkFields(['name', 'username_template']); checkTtl(['default_ttl', 'max_ttl']); - checkFields(['creation_ldif', 'deletion_ldif', 'rollback_ldif'], '.CodeMirror-code'); + checkFields(['creation_ldif', 'deletion_ldif', 'rollback_ldif'], '.cm-content'); }); test('it should go back to list route and clean up model on cancel', async function (assert) { @@ -124,7 +122,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h const spy = sinon.spy(this.model, 'rollbackAttributes'); await this.renderComponent(); - await click('[data-test-cancel]'); + await click(GENERAL.cancelButton); assert.ok(spy.calledOnce, 'Model is rolled back on cancel'); assert.ok(this.transitionCalledWith('roles'), 'Transitions to roles list route on cancel'); @@ -133,7 +131,7 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h test('it should validate form fields', async function (assert) { const renderAndAssert = async (fields) => { await this.renderComponent(); - await click('[data-test-submit]'); + await click(GENERAL.submitButton); fields.forEach((field) => { assert.dom(GENERAL.validationErrorByAttr(field)).exists('Validation message renders'); @@ -163,11 +161,11 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h this.model = this.newModel; await this.renderComponent(); - await fillIn('[data-test-input="name"]', 'test-role'); - await fillIn('[data-test-input="dn"]', 'foo'); - await fillIn('[data-test-input="username"]', 'bar'); - await fillIn('[data-test-ttl-value="Rotation period"]', 5); - await click('[data-test-submit]'); + await fillIn(GENERAL.inputByAttr('name'), 'test-role'); + await fillIn(GENERAL.inputByAttr('dn'), 'foo'); + await fillIn(GENERAL.inputByAttr('username'), 'bar'); + await fillIn(GENERAL.ttl.input('Rotation period'), 5); + await click(GENERAL.submitButton); assert.ok( this.transitionCalledWith('roles.role.details', 'static', 'test-role'), @@ -187,10 +185,10 @@ module('Integration | Component | ldap | Page::Role::CreateAndEdit', function (h this.model = this.fetchModel('static', 'static-role'); await this.renderComponent(); - await fillIn('[data-test-input="dn"]', 'foo'); - await fillIn('[data-test-input="username"]', 'bar'); - await fillIn('[data-test-ttl-value="Rotation period"]', 30); - await click('[data-test-submit]'); + await fillIn(GENERAL.inputByAttr('dn'), 'foo'); + await fillIn(GENERAL.inputByAttr('username'), 'bar'); + await fillIn(GENERAL.ttl.input('Rotation period'), 30); + await click(GENERAL.submitButton); assert.ok( this.transitionCalledWith('roles.role.details', 'static', 'test-role'), diff --git a/ui/tests/integration/components/oidc/scope-form-test.js b/ui/tests/integration/components/oidc/scope-form-test.js index 987673904f..16520f1960 100644 --- a/ui/tests/integration/components/oidc/scope-form-test.js +++ b/ui/tests/integration/components/oidc/scope-form-test.js @@ -55,14 +55,12 @@ module('Integration | Component | oidc/scope-form', function (hooks) { // 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"]') + .dom(`${GENERAL.inputByAttr('template')} .hds-code-editor__header`) .exists('JsonEditor toolbar renders'); - assert - .dom('[data-test-input="template"] [data-test-component="code-mirror-modifier"]') - .exists('Code mirror renders'); + assert.dom(`${GENERAL.inputByAttr('template')} ${GENERAL.codemirror}`).exists('Code mirror renders'); - await fillIn('[data-test-input="name"]', 'test'); - await fillIn('[data-test-input="description"]', 'this is a test'); + await fillIn(GENERAL.inputByAttr('name'), 'test'); + await fillIn(GENERAL.inputByAttr('description'), 'this is a test'); await click(SELECTORS.scopeSaveButton); }); @@ -92,20 +90,20 @@ module('Integration | Component | oidc/scope-form', function (hooks) { assert.dom('[data-test-oidc-scope-title]').hasText('Edit Scope', 'Form title renders'); assert.dom(SELECTORS.scopeSaveButton).hasText('Update', 'Save button has correct label'); - assert.dom('[data-test-input="name"]').isDisabled('Name input is disabled when editing'); - assert.dom('[data-test-input="name"]').hasValue('test', 'Name input is populated with model value'); + assert.dom(GENERAL.inputByAttr('name')).isDisabled('Name input is disabled when editing'); + assert.dom(GENERAL.inputByAttr('name')).hasValue('test', 'Name input is populated with model value'); assert - .dom('[data-test-input="description"]') + .dom(GENERAL.inputByAttr('description')) .hasValue('this is a test', 'Description input is populated with model value'); // 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"]') + .dom(`${GENERAL.inputByAttr('template')} .hds-code-editor__header`) .exists('JsonEditor toolbar renders'); assert - .dom('[data-test-input="template"] [data-test-component="code-mirror-modifier"]') + .dom(`${GENERAL.inputByAttr('template')} [data-test-component="code-mirror-modifier"]`) .exists('Code mirror renders'); - await fillIn('[data-test-input="description"]', 'this is an edit test'); + await fillIn(GENERAL.inputByAttr('description'), 'this is an edit test'); await click(SELECTORS.scopeSaveButton); }); @@ -142,7 +140,7 @@ module('Integration | Component | oidc/scope-form', function (hooks) { /> `); - await fillIn('[data-test-input="description"]', 'changed description attribute'); + await fillIn(GENERAL.inputByAttr('description'), 'changed description attribute'); await click(SELECTORS.scopeCancelButton); assert.strictEqual( this.model.description, @@ -184,11 +182,11 @@ module('Integration | Component | oidc/scope-form', function (hooks) { @onSave={{this.onSave}} /> `); - await fillIn('[data-test-input="name"]', 'test-scope'); + await fillIn(GENERAL.inputByAttr('name'), 'test-scope'); await click(SELECTORS.scopeSaveButton); assert - .dom(SELECTORS.inlineAlert) + .dom(GENERAL.inlineAlert) .hasText('There was an error submitting this form.', 'form error alert renders '); - assert.dom('[data-test-message-error]').exists('alert banner renders'); + assert.dom(GENERAL.messageError).exists('alert banner renders'); }); }); diff --git a/ui/tests/integration/components/policy-form-test.js b/ui/tests/integration/components/policy-form-test.js index be24ed2aed..3d96759147 100644 --- a/ui/tests/integration/components/policy-form-test.js +++ b/ui/tests/integration/components/policy-form-test.js @@ -5,12 +5,13 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { click, fillIn, render, triggerEvent } from '@ember/test-helpers'; +import { click, fillIn, render, settled, triggerEvent, waitFor } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import sinon from 'sinon'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { overrideResponse } from 'vault/tests/helpers/stubs'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; const SELECTORS = { nameInput: '[data-test-policy-input="name"]', @@ -28,6 +29,13 @@ const SELECTORS = { pathsInput: (index) => `[data-test-string-list-input="${index}"]`, }; +async function setEditorValue(value) { + await waitFor('.cm-editor'); + const editor = codemirror(SELECTORS.policyEditor); + setCodeEditorValue(editor, value); + return settled(); +} + module('Integration | Component | policy-form', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); @@ -65,7 +73,7 @@ module('Integration | Component | policy-form', function (hooks) { assert.dom(SELECTORS.uploadFileToggle).exists({ count: 1 }, 'Upload file toggle exists'); await fillIn(SELECTORS.nameInput, 'Foo'); assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input'); - await fillIn(`${SELECTORS.policyEditor} textarea`, policy); + await setEditorValue(policy); assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model'); assert.ok(this.onSave.notCalled); assert.dom(GENERAL.submitButton).hasText('Create policy'); @@ -93,7 +101,7 @@ module('Integration | Component | policy-form', function (hooks) { assert.dom(SELECTORS.uploadFileToggle).exists({ count: 1 }, 'Upload file toggle exists'); await fillIn(SELECTORS.nameInput, 'Foo'); assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input'); - await fillIn(`${SELECTORS.policyEditor} textarea`, policy); + await setEditorValue(policy); assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model'); assert.ok(this.onSave.notCalled); assert.dom(GENERAL.submitButton).hasText('Create policy'); @@ -121,7 +129,7 @@ module('Integration | Component | policy-form', function (hooks) { assert.dom(SELECTORS.uploadFileToggle).exists({ count: 1 }, 'Upload file toggle exists'); await fillIn(SELECTORS.nameInput, 'Foo'); assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input'); - await fillIn(`${SELECTORS.policyEditor} textarea`, policy); + await setEditorValue(policy); assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model'); assert.dom(SELECTORS.fields('paths')).exists('Paths field exists'); assert.dom(SELECTORS.pathsInput('0')).exists('0 field exists'); @@ -157,22 +165,6 @@ module('Integration | Component | policy-form', function (hooks) { assert.propEqual(this.onSave.lastCall.args[0].policy, policy, 'policy content saves in correct format'); }); - test('it shows alt + tab message only when json editor is visible', async function (assert) { - await render(hbs` - - `); - assert.dom(SELECTORS.altTabMessage).exists({ count: 1 }, 'Alt tab message shows'); - assert.dom(SELECTORS.policyEditor).exists({ count: 1 }, 'Policy editor is shown'); - - await click(SELECTORS.uploadFileToggle); - assert.dom(SELECTORS.policyUpload).exists({ count: 1 }, 'Policy upload is shown after toggle'); - assert.dom(SELECTORS.altTabMessage).doesNotExist('Alt tab message is not shown'); - }); - test('it renders the form to edit existing ACL policy', async function (assert) { const model = this.store.createRecord('policy/acl', { name: 'bar', @@ -191,12 +183,8 @@ module('Integration | Component | policy-form', function (hooks) { assert.dom(SELECTORS.nameInput).doesNotExist('Name input is not rendered'); assert.dom(SELECTORS.uploadFileToggle).doesNotExist('Upload file toggle does not exist'); - await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-'); - assert.strictEqual( - this.model.policy, - 'updated-some policy content', - 'Policy editor updates policy value on model' - ); + await setEditorValue('updated'); + assert.strictEqual(this.model.policy, 'updated', 'Policy editor updates policy value on model'); assert.ok(this.onSave.notCalled); assert.dom(GENERAL.submitButton).hasText('Save', 'Save button text is correct'); await click(GENERAL.submitButton); @@ -221,12 +209,8 @@ module('Integration | Component | policy-form', function (hooks) { assert.dom(SELECTORS.nameInput).doesNotExist('Name input is not rendered'); assert.dom(SELECTORS.uploadFileToggle).doesNotExist('Upload file toggle does not exist'); - await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-'); - assert.strictEqual( - this.model.policy, - 'updated-some policy content', - 'Policy editor updates policy value on model' - ); + await setEditorValue('updated'); + assert.strictEqual(this.model.policy, 'updated', 'Policy editor updates policy value on model'); assert.ok(this.onSave.notCalled); assert.dom(GENERAL.submitButton).hasText('Save', 'Save button text is correct'); await click(GENERAL.submitButton); @@ -251,12 +235,8 @@ module('Integration | Component | policy-form', function (hooks) { `); assert.dom(SELECTORS.nameInput).doesNotExist('Name input is not rendered'); assert.dom(SELECTORS.uploadFileToggle).doesNotExist('Upload file toggle does not exist'); - await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-'); - assert.strictEqual( - this.model.policy, - 'updated-some policy content', - 'Policy editor updates policy value on model' - ); + await setEditorValue('updated'); + assert.strictEqual(this.model.policy, 'updated', 'Policy editor updates policy value on model'); await fillIn(SELECTORS.pathsInput('1'), 'second path'); assert.strictEqual( JSON.stringify(this.model.paths), @@ -303,7 +283,7 @@ module('Integration | Component | policy-form', function (hooks) { `); await fillIn(SELECTORS.nameInput, 'Foo'); assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input'); - await fillIn(`${SELECTORS.policyEditor} textarea`, policy); + await setEditorValue(policy); assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model'); await click(GENERAL.cancelButton); @@ -326,12 +306,8 @@ module('Integration | Component | policy-form', function (hooks) { @onSave={{this.onSave}} /> `); - await fillIn(`${SELECTORS.policyEditor} textarea`, 'updated-'); - assert.strictEqual( - this.model.policy, - 'updated-some policy content', - 'Policy editor updates policy value on model' - ); + await setEditorValue('updated'); + assert.strictEqual(this.model.policy, 'updated', 'Policy editor updates policy value on model'); await click(GENERAL.cancelButton); assert.ok(this.onSave.notCalled); assert.ok(this.onCancel.calledOnce, 'Form calls onCancel'); diff --git a/ui/tests/integration/components/search-select-with-modal-test.js b/ui/tests/integration/components/search-select-with-modal-test.js index cd2431a8a7..58e38cc830 100644 --- a/ui/tests/integration/components/search-select-with-modal-test.js +++ b/ui/tests/integration/components/search-select-with-modal-test.js @@ -9,12 +9,13 @@ import { create } from 'ember-cli-page-object'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { Response } from 'miragejs'; import { clickTrigger, typeInSearch } from 'ember-power-select/test-support/helpers'; -import { render, fillIn, click, findAll } from '@ember/test-helpers'; +import { render, fillIn, click, findAll, waitFor, settled } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import ss from 'vault/tests/pages/components/search-select'; import sinon from 'sinon'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; const component = create(ss); @@ -157,7 +158,7 @@ module('Integration | Component | search select with modal', function (hooks) { await component.selectOption(); assert.dom('#search-select-modal').exists('modal is active'); - assert.dom('[data-test-empty-state-title]').hasText('No policy type selected'); + assert.dom(GENERAL.emptyStateTitle).hasText('No policy type selected'); assert.ok(this.onChange.notCalled, 'onChange is not called'); }); @@ -194,21 +195,21 @@ module('Integration | Component | search select with modal', function (hooks) { 'dropdown gives option to create new option' ); await component.selectOption(); - assert.dom('[data-test-empty-state-title]').hasText('No policy type selected'); - await fillIn('[data-test-select="policyType"]', 'acl'); + assert.dom(GENERAL.emptyStateTitle).hasText('No policy type selected'); + await fillIn(GENERAL.selectByAttr('policyType'), 'acl'); assert.dom('[data-test-policy-form]').exists('policy form renders after type is selected'); await click('[data-test-tab-example-policy] button'); assert.dom('[data-test-tab-example-policy] button').hasAttribute('aria-selected', 'true'); await click('[data-test-tab-your-policy] button'); assert.dom('[data-test-tab-your-policy] button').hasAttribute('aria-selected', 'true'); - await fillIn( - '[data-test-component="code-mirror-modifier"] textarea', - 'path "secret/super-secret" { capabilities = ["deny"] }' - ); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, 'path "secret/super-secret" { capabilities = ["deny"] }'); + await settled(); await click(GENERAL.submitButton); assert.dom('[data-test-modal-div]').doesNotExist('modal closes after save'); assert - .dom('[data-test-selected-option="0"]') + .dom(GENERAL.searchSelect.selectedOption(0)) .hasText('acl-test-new', 'adds newly created policy to selected options'); assert.ok( this.onChange.calledWithExactly(['acl-test-new']), diff --git a/ui/tests/integration/components/secret-edit-test.js b/ui/tests/integration/components/secret-edit-test.js index 22d82b979e..a59d6c02b9 100644 --- a/ui/tests/integration/components/secret-edit-test.js +++ b/ui/tests/integration/components/secret-edit-test.js @@ -5,13 +5,13 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, settled } from '@ember/test-helpers'; +import { render, settled, waitFor } from '@ember/test-helpers'; import { resolve } from 'rsvp'; import { run } from '@ember/runloop'; import Service from '@ember/service'; import hbs from 'htmlbars-inline-precompile'; -import codemirror from 'vault/tests/helpers/codemirror'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror'; let capabilities; const storeService = Service.extend({ @@ -69,11 +69,11 @@ module('Integration | Component | secret edit', function (hooks) { hbs`` ); - codemirror().setValue(JSON.stringify([{ foo: 'bar' }])); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, JSON.stringify([{ foo: 'bar' }])); await settled(); - assert - .dom('[data-test-message-error]') - .includesText('Vault expects data to be formatted as an JSON object'); + assert.dom(GENERAL.messageError).includesText('Vault expects data to be formatted as an JSON object'); }); test('it allows saving when the model isError', async function (assert) { @@ -108,10 +108,10 @@ module('Integration | Component | secret edit', function (hooks) { hbs`` ); - codemirror().setValue(JSON.stringify([{ foo: 'bar' }])); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, JSON.stringify([{ foo: 'bar' }])); await settled(); - assert - .dom('[data-test-message-error]') - .includesText('Vault expects data to be formatted as an JSON object'); + assert.dom(GENERAL.messageError).includesText('Vault expects data to be formatted as an JSON object'); }); }); diff --git a/ui/tests/integration/components/tools/unwrap-test.js b/ui/tests/integration/components/tools/unwrap-test.js index f794926da2..24f31a0221 100644 --- a/ui/tests/integration/components/tools/unwrap-test.js +++ b/ui/tests/integration/components/tools/unwrap-test.js @@ -7,10 +7,10 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { Response } from 'miragejs'; -import { click, fillIn, find, render, waitUntil } from '@ember/test-helpers'; +import { click, fillIn, find, render, settled, waitFor, waitUntil } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; -import codemirror from 'vault/tests/helpers/codemirror'; +import { assertCodeBlockValue } from 'vault/tests/helpers/codemirror'; import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors'; import sinon from 'sinon'; @@ -72,11 +72,11 @@ module('Integration | Component | tools/unwrap', function (hooks) { // test submit await fillIn(TS.toolsInput('unwrap-token'), data.token); await click(GENERAL.submitButton); - - await waitUntil(() => find('.CodeMirror')); + await waitFor('.hds-code-block'); assert.true(flashSpy.calledWith('Unwrap was successful.'), 'it renders success flash'); - assert.dom('label').hasText('Unwrapped Data'); - assert.strictEqual(codemirror().getValue(' '), '{ "foo": "bar" }', 'it renders unwrapped data'); + assert.dom('.hds-code-block__title').hasText('Unwrapped Data'); + await settled(); + assertCodeBlockValue(assert, '.hds-code-block__code', '{ "foo": "bar" }'); assert.dom(GENERAL.hdsTab('data')).hasAttribute('aria-selected', 'true'); await click(GENERAL.hdsTab('details')); @@ -119,9 +119,7 @@ module('Integration | Component | tools/unwrap', function (hooks) { await fillIn(TS.toolsInput('unwrap-token'), data.token); await click(GENERAL.submitButton); - - await waitUntil(() => find('.CodeMirror')); - await click(GENERAL.hdsTab('details')); + await waitFor('.hds-code-block'); assert .dom(`${GENERAL.infoRowValue('Renewable')} ${GENERAL.icon('check-circle')}`) .exists('renders truthy icon for renewable'); diff --git a/ui/tests/integration/components/tools/wrap-test.js b/ui/tests/integration/components/tools/wrap-test.js index 7918829296..169be56252 100644 --- a/ui/tests/integration/components/tools/wrap-test.js +++ b/ui/tests/integration/components/tools/wrap-test.js @@ -7,14 +7,21 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { Response } from 'miragejs'; -import { click, fillIn, find, render, waitUntil } from '@ember/test-helpers'; +import { click, fillIn, find, render, settled, waitFor, waitUntil } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { TTL_PICKER as TTL } from 'vault/tests/helpers/components/ttl-picker-selectors'; import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; import sinon from 'sinon'; +async function setEditorValue(value) { + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, value); + return settled(); +} + module('Integration | Component | tools/wrap', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); @@ -36,12 +43,21 @@ module('Integration | Component | tools/wrap', function (hooks) { test('it renders defaults', async function (assert) { await this.renderComponent(); + await waitFor('.cm-editor'); + assert.dom('h1').hasText('Wrap Data', 'Title renders'); assert.dom('[data-test-toggle-label="json"]').hasText('JSON'); - assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap (json-formatted)'); + assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap'); + assert.dom('.hds-code-editor__description').hasText('json-formatted'); + + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); + assert.strictEqual( - codemirror().getValue(' '), - `{ \"\": \"\" }`, // eslint-disable-line no-useless-escape + editorValue, + `{ + "": "" +}`, 'json editor initializes with empty object that includes whitespace' ); assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); @@ -83,7 +99,7 @@ module('Integration | Component | tools/wrap', function (hooks) { }); await this.renderComponent(); - await codemirror().setValue(this.wrapData); + await setEditorValue(this.wrapData); await click(GENERAL.submitButton); await waitUntil(() => find(TS.toolsInput('wrapping-token'))); assert.true(flashSpy.calledWith('Wrap was successful.'), 'it renders success flash'); @@ -103,26 +119,34 @@ module('Integration | Component | tools/wrap', function (hooks) { }); await this.renderComponent(); - await codemirror().setValue(this.wrapData); + await setEditorValue(this.wrapData); await click(TTL.toggleByLabel('Wrap TTL')); await fillIn(TTL.valueInputByLabel('Wrap TTL'), '20'); await click(GENERAL.submitButton); }); test('it toggles between views and preserves input data', async function (assert) { - assert.expect(6); + assert.expect(7); await this.renderComponent(); - await codemirror().setValue(this.wrapData); - assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap (json-formatted)'); + await setEditorValue(this.wrapData); + assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap'); + assert.dom('.hds-code-editor__description').hasText('json-formatted'); await click(GENERAL.toggleInput('json')); + assert.dom('[data-test-component="json-editor-title"]').doesNotExist(); - assert.dom('[data-test-kv-key="0"]').hasValue('foo'); - assert.dom('[data-test-kv-value="0"]').hasValue('bar'); + assert.dom(GENERAL.kvObjectEditor.key('0')).hasValue('foo'); + assert.dom(GENERAL.kvObjectEditor.value('0')).hasValue('bar'); await click(GENERAL.toggleInput('json')); assert.dom('[data-test-component="json-editor-title"]').exists(); + + await waitFor('.cm-editor'); + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); assert.strictEqual( - codemirror().getValue(' '), - `{ \"foo": \"bar" }`, // eslint-disable-line no-useless-escape + editorValue, + `{ + "foo": "bar" +}`, 'json editor has original data' ); }); @@ -157,11 +181,11 @@ module('Integration | Component | tools/wrap', function (hooks) { await this.renderComponent(); await click(GENERAL.toggleInput('json')); - await fillIn('[data-test-kv-key="0"]', 'foo'); - await fillIn('[data-test-kv-value="0"]', 'bar'); + await fillIn(GENERAL.kvObjectEditor.key('0'), 'foo'); + await fillIn(GENERAL.kvObjectEditor.value('0'), 'bar'); await click('[data-test-kv-add-row="0"]'); - await fillIn('[data-test-kv-key="1"]', 'foo2'); - await fillIn('[data-test-kv-value="1"]', multilineData); + await fillIn(GENERAL.kvObjectEditor.key('1'), 'foo2'); + await fillIn(GENERAL.kvObjectEditor.value('1'), multilineData); await click(GENERAL.submitButton); await waitUntil(() => find(TS.toolsInput('wrapping-token'))); assert.true(flashSpy.calledWith('Wrap was successful.'), 'it renders success flash'); @@ -172,16 +196,21 @@ module('Integration | Component | tools/wrap', function (hooks) { test('it resets on done', async function (assert) { await this.renderComponent(); - await codemirror().setValue(this.wrapData); + await setEditorValue(this.wrapData); await click(TTL.toggleByLabel('Wrap TTL')); await fillIn(TTL.valueInputByLabel('Wrap TTL'), '20'); await click(GENERAL.submitButton); await waitUntil(() => find(GENERAL.button('Done'))); await click(GENERAL.button('Done')); + await waitFor('.cm-editor'); + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); assert.strictEqual( - codemirror().getValue(' '), - `{ \"\": \"\" }`, // eslint-disable-line no-useless-escape + editorValue, + `{ + "": "" +}`, 'json editor initializes with empty object that includes whitespace' ); assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL resets to unchecked'); @@ -191,14 +220,19 @@ module('Integration | Component | tools/wrap', function (hooks) { test('it preserves input data on back', async function (assert) { await this.renderComponent(); - await codemirror().setValue(this.wrapData); + await setEditorValue(this.wrapData); await click(GENERAL.submitButton); await waitUntil(() => find(GENERAL.button('Back'))); await click(GENERAL.button('Back')); + await waitFor('.cm-editor'); + const editor = codemirror(); + const editorValue = getCodeEditorValue(editor); assert.strictEqual( - codemirror().getValue(' '), - `{ \"foo": \"bar" }`, // eslint-disable-line no-useless-escape + editorValue, + `{ + "foo": "bar" +}`, 'json editor has original data' ); assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); @@ -206,22 +240,22 @@ module('Integration | Component | tools/wrap', function (hooks) { test('it renders/hides warning based on json linting', async function (assert) { await this.renderComponent(); - await codemirror().setValue(`{bad json}`); + await setEditorValue(`{bad json}`); assert - .dom('[data-test-inline-alert]') + .dom(GENERAL.inlineAlert) .hasText( 'JSON is unparsable. Fix linting errors to avoid data discrepancies.', 'Linting error message is shown for json view' ); - await codemirror().setValue(this.wrapData); - assert.dom('[data-test-inline-alert]').doesNotExist(); + await setEditorValue(this.wrapData); + assert.dom(GENERAL.inlineAlert).doesNotExist(); }); test('it hides json warning on back and on done', async function (assert) { await this.renderComponent(); - await codemirror().setValue(`{bad json}`); + await setEditorValue(`{bad json}`); assert - .dom('[data-test-inline-alert]') + .dom(GENERAL.inlineAlert) .hasText( 'JSON is unparsable. Fix linting errors to avoid data discrepancies.', 'Linting error message is shown for json view' @@ -229,11 +263,11 @@ module('Integration | Component | tools/wrap', function (hooks) { await click(GENERAL.submitButton); await waitUntil(() => find(GENERAL.button('Done'))); await click(GENERAL.button('Done')); - assert.dom('[data-test-inline-alert]').doesNotExist(); + assert.dom(GENERAL.inlineAlert).doesNotExist(); - await codemirror().setValue(`{bad json}`); + await setEditorValue(`{bad json}`); assert - .dom('[data-test-inline-alert]') + .dom(GENERAL.inlineAlert) .hasText( 'JSON is unparsable. Fix linting errors to avoid data discrepancies.', 'Linting error message is shown for json view' @@ -241,6 +275,6 @@ module('Integration | Component | tools/wrap', function (hooks) { await click(GENERAL.submitButton); await waitUntil(() => find(GENERAL.button('Back'))); await click(GENERAL.button('Back')); - assert.dom('[data-test-inline-alert]').doesNotExist(); + assert.dom(GENERAL.inlineAlert).doesNotExist(); }); }); diff --git a/ui/tests/integration/components/transit-key-actions-test.js b/ui/tests/integration/components/transit-key-actions-test.js index bd1d2c3b3d..f7dae0c229 100644 --- a/ui/tests/integration/components/transit-key-actions-test.js +++ b/ui/tests/integration/components/transit-key-actions-test.js @@ -8,11 +8,12 @@ import { resolve } from 'rsvp'; import Service from '@ember/service'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, click, find, fillIn, blur, triggerEvent } from '@ember/test-helpers'; +import { render, click, find, fillIn, blur, triggerEvent, waitFor } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import { encodeString } from 'vault/utils/b64'; import waitForError from 'vault/tests/helpers/wait-for-error'; -import codemirror from 'vault/tests/helpers/codemirror'; +import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; const storeStub = Service.extend({ callArgs: null, @@ -149,7 +150,10 @@ module('Integration | Component | transit key actions', function (hooks) { await render(hbs` `); - codemirror('#plaintext-control').setValue('plaintext'); + let editor; + await waitFor('.cm-editor'); + editor = codemirror('#plaintext-control'); + setCodeEditorValue(editor, 'plaintext'); await click('button[type="submit"]'); assert.deepEqual( this.storeService.callArgs, @@ -170,7 +174,9 @@ module('Integration | Component | transit key actions', function (hooks) { await click('dialog button'); // Encrypt again, with pre-encoded value and checkbox selected const preEncodedValue = encodeString('plaintext'); - codemirror('#plaintext-control').setValue(preEncodedValue); + await waitFor('.cm-editor'); + editor = codemirror('#plaintext-control'); + setCodeEditorValue(editor, preEncodedValue); await click('input[data-test-transit-input="encodedBase64"]'); await click('button[type="submit"]'); @@ -200,7 +206,9 @@ module('Integration | Component | transit key actions', function (hooks) { await render(hbs` `); - codemirror().setValue('plaintext'); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, 'plaintext'); assert.dom('#key_version').exists({ count: 1 }, 'it renders the key version selector'); await triggerEvent('#key_version', 'change'); @@ -229,7 +237,9 @@ module('Integration | Component | transit key actions', function (hooks) { await render(hbs` `); - codemirror('#plaintext-control').setValue('plaintext'); + await waitFor('.cm-editor'); + const editor = codemirror('#plaintext-control'); + setCodeEditorValue(editor, 'plaintext'); assert.dom('#key_version').doesNotExist('it does not render the selector when there is only one key'); }); @@ -240,7 +250,9 @@ module('Integration | Component | transit key actions', function (hooks) { this.set('storeService.keyActionReturnVal', { plaintext }); this.set('selectedAction', 'decrypt'); - assert.strictEqual(codemirror('#ciphertext-control').getValue(), '', 'does not prefill ciphertext value'); + await waitFor('.cm-editor'); + const editor = codemirror('#ciphertext-control'); + assert.strictEqual(getCodeEditorValue(editor), '', 'does not prefill ciphertext value'); }); const setupExport = async function () { @@ -285,7 +297,7 @@ module('Integration | Component | transit key actions', function (hooks) { this.set('storeService.keyActionReturnVal', response); await setupExport.call(this); await click('[data-test-toggle-label="Wrap response"]'); - await click('button[type="submit"]'); + await click(GENERAL.submitButton); assert.dom('#transit-export-modal').exists('Modal opens after export'); assert.deepEqual( JSON.parse(find('[data-test-encrypted-value="export"]').innerText), @@ -301,7 +313,7 @@ module('Integration | Component | transit key actions', function (hooks) { await click('[data-test-toggle-label="Wrap response"]'); await click('#exportVersion'); await triggerEvent('#exportVersion', 'change'); - await click('button[type="submit"]'); + await click(GENERAL.submitButton); assert.dom('#transit-export-modal').exists('Modal opens after export'); assert.deepEqual( JSON.parse(find('[data-test-encrypted-value="export"]').innerText), @@ -335,9 +347,11 @@ module('Integration | Component | transit key actions', function (hooks) { `); await fillIn('#algorithm', 'sha2-384'); await blur('#algorithm'); - await fillIn('[data-test-component="code-mirror-modifier"] textarea', 'plaintext'); + await waitFor('.cm-editor'); + const editor = codemirror(); + setCodeEditorValue(editor, 'plaintext'); await click('input[data-test-transit-input="encodedBase64"]'); - await click('button[type="submit"]'); + await click(GENERAL.submitButton); assert.deepEqual( this.storeService.callArgs, { diff --git a/ui/tests/pages/components/json-editor.js b/ui/tests/pages/components/json-editor.js index b410c19270..aa74713a4f 100644 --- a/ui/tests/pages/components/json-editor.js +++ b/ui/tests/pages/components/json-editor.js @@ -3,11 +3,9 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { isPresent, notHasClass, text } from 'ember-cli-page-object'; - -export default { - title: text('[data-test-component=json-editor-title]'), - hasToolbar: isPresent('[data-test-component=json-editor-toolbar]'), - hasJSONEditor: isPresent('[data-test-component="code-mirror-modifier"]'), - canEdit: notHasClass('readonly-codemirror'), +export const SELECTORS = { + codeBlock: '.hds-code-block__code', + copy: '.hds-code-block__copy-button', + title: '[data-test-component="json-editor-title"]', + toolbar: '.hds-code-block__header', }; diff --git a/ui/yarn.lock b/ui/yarn.lock index 80bc62c968..d4bc498492 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -3529,15 +3529,6 @@ __metadata: languageName: node linkType: hard -"@types/codemirror@npm:~5.60.15": - version: 5.60.15 - resolution: "@types/codemirror@npm:5.60.15" - dependencies: - "@types/tern": "*" - checksum: cfad3f569de48fba3efa44fdfeba77933e231486a52cc80cff7ce6eeeed5b447a5bc2b11e2226bc00ccee332c661e53e35a15cf14eb835f434a6a402d9462f5f - languageName: node - linkType: hard - "@types/connect@npm:*": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" @@ -4049,15 +4040,6 @@ __metadata: languageName: node linkType: hard -"@types/tern@npm:*": - version: 0.23.9 - resolution: "@types/tern@npm:0.23.9" - dependencies: - "@types/estree": "*" - checksum: 53f229c79edf9454011f5b37c8539e0e760a130beac953d4e2126823de1ac6b0e2a45612596679fb232ec861826584fcaa272e2254a890b410575683423d56a8 - languageName: node - linkType: hard - "@types/tmp@npm:^0.0.33": version: 0.0.33 resolution: "@types/tmp@npm:0.0.33" @@ -6726,13 +6708,6 @@ __metadata: languageName: node linkType: hard -"codemirror@npm:~5.65.19": - version: 5.65.19 - resolution: "codemirror@npm:5.65.19" - checksum: 3422332a62d301224e7061edded5fc7596e2a97cdb8186f725d479481bbc43bbe1e61150955579432455b3fd653822c39ec8d4e090c62fc5d36c753f6308374f - languageName: node - linkType: hard - "collect-all@npm:^1.0.4": version: 1.0.4 resolution: "collect-all@npm:1.0.4" @@ -18486,7 +18461,6 @@ __metadata: "@icholy/duration": ~5.1.0 "@lineal-viz/lineal": ~0.5.1 "@tsconfig/ember": ~2.0.0 - "@types/codemirror": ~5.60.15 "@types/d3-array": ~3.2.1 "@types/ember-data": ~4.4.16 "@types/qunit": ~2.19.12 @@ -18500,7 +18474,6 @@ __metadata: base64-js: ~1.5.1 broccoli-asset-rev: ~3.0.0 broccoli-sri-hash: "meirish/broccoli-sri-hash#rooturl" - codemirror: ~5.65.19 columnify: ~1.6.0 concurrently: ~9.1.2 d3-array: ~3.2.4