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 <lane.wetmore@hashicorp.com>
This commit is contained in:
Zack Moore 2025-07-23 11:12:20 -07:00 committed by GitHub
parent 881febbf98
commit e6ce95acd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 732 additions and 1013 deletions

3
changelog/30188.txt Normal file
View file

@ -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
```

View file

@ -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
}
}

View file

@ -23,13 +23,7 @@
</div>
{{/if}}
<div class="field">
{{#unless this.showFileUpload}}
<span class="is-size-9 has-text-grey has-bottom-margin-l has-top-margin-xs" data-test-alt-tab-message>
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field.
</span>
{{/unless}}
<Toolbar aria-label="toolbar for managing {{or @model.name 'new'}} policy">
<label class="has-text-weight-bold has-right-margin-4">Policy</label>
{{#if @renderPolicyExampleModal}}
{{! only true in policy create and edit routes }}
<ToolbarFilters aria-label="help tools for managing {{or @model.name 'new'}} policy">
@ -43,9 +37,8 @@
/>
</ToolbarFilters>
{{/if}}
<ToolbarActions aria-label="actions for managing {{or @model.name 'new'}} policy">
<div class="toolbar-separator"></div>
{{#if @model.isNew}}
{{#if @model.isNew}}
<ToolbarActions aria-label="actions for managing {{or @model.name 'new'}} policy">
<div class="control is-flex">
<Input
id="fileUploadToggle"
@ -58,20 +51,8 @@
/>
<label for="fileUploadToggle" class="has-text-weight-bold is-size-8">Upload file</label>
</div>
{{else}}
{{! EDITING - no file upload toggle}}
<Hds::Copy::Button
@text="Copy"
@isIconOnly={{true}}
@textToCopy={{@model.policy}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")
)}}
class="transparent"
data-test-copy-button
/>
{{/if}}
</ToolbarActions>
</ToolbarActions>
{{/if}}
</Toolbar>
{{#if this.showFileUpload}}
<div class="has-top-margin-xs">
@ -80,7 +61,6 @@
{{else}}
<JsonEditor
@title="Policy"
@showToolbar={{false}}
@value={{@model.policy}}
@valueUpdated={{action (mut @model.policy)}}
@mode="ruby"
@ -88,7 +68,6 @@
data-test-policy-editor
/>
{{/if}}
</div>
{{#each @model.additionalAttrs as |attr|}}
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />

View file

@ -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
}
},
},

View file

@ -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
}
},
},

View file

@ -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;

View file

@ -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(

View file

@ -53,7 +53,7 @@
@title="Data to wrap"
@subTitle="json-formatted"
@value={{this.stringifiedWrapData}}
@valueUpdated={{this.codemirrorUpdated}}
@valueUpdated={{this.editorUpdated}}
/>
{{else}}
<KvObjectEditor

View file

@ -13,7 +13,6 @@ import type ApiService from 'vault/services/api';
import type FlashMessageService from 'vault/services/flash-messages';
import type { TtlEvent } from 'vault/app-types';
import type { HTMLElementEvent } from 'vault/forms';
import type { Editor } from 'codemirror';
/**
* @module ToolsWrap
@ -68,10 +67,14 @@ export default class ToolsWrap extends Component {
}
@action
codemirrorUpdated(val: string, codemirror: Editor) {
codemirror.performLint();
this.hasLintingErrors = codemirror?.state.lint.marked?.length > 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

View file

@ -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
}
},

View file

@ -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;
}

View file

@ -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 {

View file

@ -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';

View file

@ -4,15 +4,7 @@
}}
<div class="console-ui-output">
<JsonEditor
@showToolbar={{false}}
@value={{stringify this.content}}
@readOnly={{true}}
{{! ideally we calculate the "height" of the json data, but 100 should cover most cases }}
@viewportMargin="100"
@gutters={{false}}
@theme="hashi auto-height"
/>
<JsonEditor @value={{stringify this.content}} @readOnly={{true}} />
<Hds::Copy::Button
@text="Copy"
@isIconOnly={{true}}

View file

@ -16,16 +16,7 @@
{{#if this.unwrapData}}
<div class="control-group-success is-editor">
<div class="is-relative">
<JsonEditor
data-test-json-viewer
@showToolbar={{false}}
@value={{stringify this.unwrapData}}
@readOnly={{true}}
{{! ideally we calculate the "height" of the json data, but 100 should cover most cases }}
@viewportMargin="100"
@gutters={{false}}
@theme="hashi-read-only auto-height"
/>
<JsonEditor data-test-json-viewer @value={{stringify this.unwrapData}} @readOnly={{true}} />
<Hds::Copy::Button
@text="Copy"
@isIconOnly={{true}}

View file

@ -80,7 +80,7 @@
@title={{capitalize (or @attr.options.label (humanize (dasherize @attr.name)))}}
@helpText={{@attr.options.helpText}}
@value={{if (get @model @attr.name) (stringify (get @model @attr.name)) @emptyData}}
@valueUpdated={{@codemirrorUpdated}}
@valueUpdated={{@editorUpdated}}
/>
{{/if}}
</div>

View file

@ -44,10 +44,6 @@
{{#each @model.formFields as |field|}}
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
{{/each}}
<p class="is-size-9 has-text-grey has-bottom-margin-l">
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.
</p>
</div>
<div class="has-top-margin-l has-bottom-margin-l">
<Hds::Button

View file

@ -53,9 +53,9 @@
<div class="form-section">
<JsonEditor
@title="Secret Data"
@value={{this.codemirrorString}}
@valueUpdated={{this.codemirrorUpdated}}
@onFocusOut={{this.formatJSON}}
@value={{this.editorString}}
@valueUpdated={{this.editorUpdated}}
@onBlur={{this.formatJSON}}
/>
</div>
{{else}}
@ -150,9 +150,9 @@
<div class="form-section">
<JsonEditor
@title="Secret Data"
@value={{this.codemirrorString}}
@valueUpdated={{this.codemirrorUpdated}}
@onFocusOut={{this.formatJSON}}
@value={{this.editorString}}
@valueUpdated={{this.editorUpdated}}
@onBlur={{this.formatJSON}}
/>
</div>
{{else}}

View file

@ -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")}}
/>

View file

@ -12,7 +12,7 @@
{{else}}
{{#if @showAdvancedMode}}
<div class="has-top-margin-s">
<JsonEditor @title="Secret Data" @value={{@modelForData.dataAsJSONString}} @readOnly={{true}} />
<JsonEditor @title="Secret Data" @value={{or @modelForData.dataAsJSONString " "}} @readOnly={{true}} />
</div>
{{else}}
<div class="info-table-row-header">

View file

@ -63,7 +63,7 @@
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<Hds::Button @text="Export key" type="submit" />
<Hds::Button @text="Export key" type="submit" data-test-submit />
</div>
</div>
</form>

View file

@ -46,7 +46,7 @@
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<Hds::Button @text="HMAC" type="submit" />
<Hds::Button @text="HMAC" type="submit" data-test-submit />
</div>
</div>
</form>

View file

@ -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}}

View file

@ -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');

View file

@ -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 @@
<JsonEditor
@title={{this.labelString}}
@value={{if (get @model this.valuePath) (stringify (get @model this.valuePath)) this.emptyData}}
@valueUpdated={{fn this.codemirrorUpdated false}}
@valueUpdated={{fn this.editorUpdated false}}
@helpText={{this.helpTextString}}
@example={{@attr.options.example}}
/>

View file

@ -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;

View file

@ -2,19 +2,58 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<div ...attributes>
{{#if this.getShowToolbar}}
<div data-test-component="json-editor-toolbar">
<Toolbar aria-label="items for managing JSON editor for {{@title}}">
<label id="json-editor-title" class="has-text-weight-bold" data-test-component="json-editor-title">
{{#if @readOnly}}
<Hds::CodeBlock @hasCopyButton={{true}} @language={{@mode}} @value={{or @value @example}} as |CB|>
{{#if @title}}
<CB.Title data-test-component="json-editor-title">
{{@title}}
{{#if @subTitle}}
<span class="is-size-9 is-lowercase has-text-grey-light">({{@subTitle}})</span>
{{/if}}
</label>
<ToolbarActions aria-label="actions for {{@title}} JSON editor">
</CB.Title>
{{/if}}
{{#if @helpText}}
<CB.Description>
{{@helpText}}
</CB.Description>
{{/if}}
</Hds::CodeBlock>
{{else}}
<Hds::CodeEditor
data-test-component="code-mirror-modifier"
@ariaLabel={{this.ariaLabel}}
@extraKeys={{@extraKeys}}
@hasCopyButton={{true}}
@language={{this.mode}}
@isLintingEnabled={{eq this.mode "json"}}
@value={{or @value @example}}
@onBlur={{@onBlur}}
@onInput={{this.onUpdate}}
@onLint={{@onLint}}
@onSetup={{this.onSetup}}
as |CE|
>
{{#if this.getShowToolbar}}
{{#if @title}}
<CE.Title data-test-component="json-editor-title">
{{@title}}
</CE.Title>
{{/if}}
{{#if @subTitle}}
<CE.Description>
{{@subTitle}}
</CE.Description>
{{/if}}
{{#if @helpText}}
<CE.Description>
{{@helpText}}
</CE.Description>
{{/if}}
<CE.Generic>
{{yield}}
{{#if @example}}
<Hds::Button
class="toolbar-button"
@ -25,45 +64,8 @@
data-test-restore-example
/>
{{/if}}
<div class="toolbar-separator"></div>
<Hds::Copy::Button
@container={{@container}}
@text="Copy"
@isIconOnly={{true}}
@textToCopy={{@value}}
@onError={{(fn
(set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")
)}}
class="transparent"
data-test-copy-button
/>
</ToolbarActions>
</Toolbar>
</div>
{{/if}}
<div
{{code-mirror
screenReaderLabel="JSON editor"
content=(or @value @example)
extraKeys=@extraKeys
gutters=@gutters
lineNumbers=(if @readOnly false true)
mode=@mode
readOnly=@readOnly
theme=@theme
viewportMargin=@viewportMargin
onSetup=this.onSetup
onUpdate=this.onUpdate
onFocus=this.onFocus
autoRefresh=(if @container true false)
}}
class={{if @readOnly "readonly-codemirror"}}
data-test-component="code-mirror-modifier"
></div>
{{#if @helpText}}
<div class="box is-shadowless is-fullwidth has-short-padding">
<p class="sub-text">{{@helpText}}</p>
</div>
</CE.Generic>
{{/if}}
</Hds::CodeEditor>
{{/if}}
</div>

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -1,6 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/modifiers/code-mirror';

View file

@ -115,12 +115,12 @@
@mode="ruby"
@valueUpdated={{fn (mut template.rules)}}
@helpText={{sanitized-html this.roleRulesHelpText}}
@onSetup={{fn (mut this.codemirrorEditor)}}
>
<Hds::Button
@icon="reload"
@text="Restore example"
@color="secondary"
class="toolbar-button"
{{on "click" this.resetRoleRules}}
data-test-restore-example
/>

View file

@ -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 =
'<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" target="_blank" rel="noopener noreferrer">available here</>';
'<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" target="_blank" rel="noopener noreferrer" class="has-text-white">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

View file

@ -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)}}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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",

View file

@ -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',

View file

@ -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"]')

View file

@ -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)}`);

View file

@ -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]);
});
});

View file

@ -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'
);
});
});
});

View file

@ -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'));

View file

@ -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);

View file

@ -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);
};

View file

@ -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`<Console::LogJson @content={{this.content}} />`);
const instance = find('[data-test-component=code-mirror-modifier]').innerText;
assert.strictEqual(instance, expectedText);
assertCodeBlockValue(assert, '.hds-code-block__code', expectedText);
});
});

View file

@ -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`<JsonEditor
@value={{this.json_blob}}
@readOnly={{false}}
@valueUpdated={{this.valueUpdated}}
@onFocusOut={{this.onFocusOut}}
/>`);
// 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`<JsonEditor
@value={{this.json_blob}}
@theme={{this.hashi-read-only-theme}}
@readOnly={{true}}
/>`);
// 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`
<JsonEditor
@value={{this.long_json}}
@mode="ruby"
@valueUpdated={{fn (mut this.value)}}
/>
`);
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`
<JsonEditor
@value={{this.long_json}}
@mode="ruby"
@valueUpdated={{fn (mut this.value)}}
@viewportMargin="100"
/>
`);
assert
.dom('.CodeMirror-code')
.containsText('key-9', 'With viewportMargin set, user can search for key-9');
});
});

View file

@ -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`<Page::Role::CreateAndEdit @model={{this.newModel}} @breadcrumbs={{this.breadcrumbs}} />`,
{ 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`<Page::Role::CreateAndEdit @model={{this.newModel}} @breadcrumbs={{this.breadcrumbs}}/>`,
{ 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`<Page::Role::CreateAndEdit @model={{this.role}} @breadcrumbs={{this.breadcrumbs}} />`, {
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);
});
});

View file

@ -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`<KvDataFields @showJson={{true}} @secret={{this.secret}} />`, { 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`<KvDataFields @showJson={{true}} @secret={{this.secret}} />`, { 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`<KvDataFields @showJson={{true}} @secret={{this.secret}} />`, { owner: this.engine });
assert.strictEqual(
codemirror().options.viewportMargin,
100,
'viewportMargin is set to 100 matching the height of the json'
);
});
});

View file

@ -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`
<KvPatch::JsonForm
@onSubmit={{this.onSubmit}}
@ -43,12 +43,21 @@ module('Integration | Component | kv | kv-patch/editor/json-form', function (hoo
/>`,
{ 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)}`);

View file

@ -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');

View file

@ -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);
});

View file

@ -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);
});

View file

@ -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'),

View file

@ -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');
});
});

View file

@ -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`
<PolicyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`);
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');

View file

@ -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']),

View file

@ -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`<SecretEdit @mode={{this.mode}} @model={{this.model}} @preferAdvancedEdit={{true}} @key={{this.key}} />`
);
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`<SecretEdit @mode={{this.mode}} @model={{this.model}} @preferAdvancedEdit={{true}} @key={{this.key}} />`
);
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');
});
});

View file

@ -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');

View file

@ -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();
});
});

View file

@ -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`
<TransitKeyActions @selectedAction={{this.selectedAction}} @key={{this.key}} />`);
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`
<TransitKeyActions @selectedAction="encrypt" @key={{this.key}} />`);
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`
<TransitKeyActions @selectedAction="encrypt" @key={{this.key}} />`);
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) {
<TransitKeyActions @key={{this.key}} @selectedAction="hmac" />`);
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,
{

View file

@ -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',
};

View file

@ -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