ui: refactor text file component (#18458)

* wip tests

* Move text-file to addon

* rename fileName to filename, initial cleanup of text-fil

* rename args, rename test selector

* fix eye-con, remove enterAsText from file object

* add tests

* move files back to original location

* rename files via git for git diff

* adjsut test

* Revert "wip tests"

This reverts commit 63716a1e647a0b01236d34322837456ef3e9db43.

* fix policy form input

* cleanup conditional

* add bottom margin

* add element id

* change arg name

* add text area input test

* add upload test to policy form

Co-authored-by: Chelsea Shaw <cshaw@hashicorp.com>
This commit is contained in:
claire bontempo
2022-12-19 15:54:59 -08:00
committed by GitHub
parent 600c1e634b
commit 5ebb6dfb0e
21 changed files with 330 additions and 239 deletions

View File

@@ -23,7 +23,7 @@ export default Component.extend({
fileHelpText: null, fileHelpText: null,
file: null, file: null,
fileName: null, filename: null,
fileSize: null, fileSize: null,
fileLastModified: null, fileLastModified: null,
@@ -54,7 +54,7 @@ export default Component.extend({
const { name, size, lastModifiedDate } = fileMeta || {}; const { name, size, lastModifiedDate } = fileMeta || {};
const fileSize = size ? filesize(size) : null; const fileSize = size ? filesize(size) : null;
this.set('file', fileAsBytes); this.set('file', fileAsBytes);
this.set('fileName', name); this.set('filename', name);
this.set('fileSize', fileSize); this.set('fileSize', fileSize);
this.set('fileLastModified', lastModifiedDate); this.set('fileLastModified', lastModifiedDate);
this.onChange(fileAsBytes, name); this.onChange(fileAsBytes, name);

View File

@@ -54,7 +54,7 @@ export default Component.extend({
// If after decoding it's not b64, we want // If after decoding it's not b64, we want
// the original as it was only encoded when we used `readAsDataURL`. // the original as it was only encoded when we used `readAsDataURL`.
const fileData = decoded.match(BASE_64_REGEX) ? decoded : b64File; const fileData = decoded.match(BASE_64_REGEX) ? decoded : b64File;
yield this.onChange(this.index, { value: fileData, fileName: filename }); yield this.onChange(this.index, { value: fileData, filename: filename });
}) })
), ),

View File

@@ -38,7 +38,7 @@
</ToolbarActions> </ToolbarActions>
</Toolbar> </Toolbar>
{{#if this.showFileUpload}} {{#if this.showFileUpload}}
<TextFile @inputOnly={{true}} @file={{this.file}} @onChange={{this.setPolicyFromFile}} /> <TextFile @uploadOnly={{true}} @onChange={{this.setPolicyFromFile}} />
{{else}} {{else}}
<JsonEditor <JsonEditor
@title="Policy" @title="Policy"

View File

@@ -25,7 +25,6 @@ export default class PolicyFormComponent extends Component {
@service flashMessages; @service flashMessages;
@tracked errorBanner = ''; @tracked errorBanner = '';
@tracked file = null;
@tracked showFileUpload = false; @tracked showFileUpload = false;
@task @task
@@ -50,11 +49,11 @@ export default class PolicyFormComponent extends Component {
} }
@action @action
setPolicyFromFile(index, fileInfo) { setPolicyFromFile(fileInfo) {
const { value, fileName } = fileInfo; const { value, filename } = fileInfo;
this.args.model.policy = value; this.args.model.policy = value;
if (!this.args.model.name) { if (!this.args.model.name) {
const trimmedFileName = trimRight(fileName, ['.json', '.txt', '.hcl', '.policy']); const trimmedFileName = trimRight(filename, ['.json', '.txt', '.hcl', '.policy']);
this.args.model.name = trimmedFileName.toLowerCase(); this.args.model.name = trimmedFileName.toLowerCase();
} }
this.showFileUpload = false; this.showFileUpload = false;

View File

@@ -1,83 +0,0 @@
import Component from '@glimmer/component';
import { set, action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { guidFor } from '@ember/object/internals';
/**
* @module TextFile
* `TextFile` components are file upload components where you can either toggle to upload a file or enter text.
*
* @example
* <TextFile
* @inputOnly={{true}}
* @helpText="help text"
* @file={{object}}
* @onChange={{action "someOnChangeFunction"}}
* @label={{"string"}}
* />
*
* @param [inputOnly] {bool} - When true, only the file input will be rendered
* @param [helpText] {string} - Text underneath label.
* @param file {object} - * Object in the shape of:
* {
* value: 'file contents here',
* fileName: 'nameOfFile.txt',
* enterAsText: boolean ability to enter as text
* }
* @param [onChange=Function.prototype] {Function|action} - A function to call when the value of the input changes.
* @param [label=null] {string} - Text to use as the label for the file input. If null, a default will be rendered.
*/
export default class TextFile extends Component {
fileHelpText = 'Select a file from your computer';
textareaHelpText = 'Enter the value as text';
elementId = guidFor(this);
index = '';
@tracked file = null;
@tracked showValue = false;
get inputOnly() {
return this.args.inputOnly || false;
}
get label() {
return this.args.label || null;
}
readFile(file) {
const reader = new FileReader();
reader.onload = () => this.setFile(reader.result, file.name);
reader.readAsText(file);
}
setFile(contents, filename) {
this.args.onChange(this.index, { value: contents, fileName: filename });
}
@action
pickedFile(e) {
e.preventDefault();
const { files } = e.target;
if (!files.length) {
return;
}
for (let i = 0, len = files.length; i < len; i++) {
this.readFile(files[i]);
}
}
@action
updateData(e) {
e.preventDefault();
const file = this.args.file;
set(file, 'value', e.target.value);
this.args.onChange(this.index, file);
}
@action
clearFile() {
this.args.onChange(this.index, { value: '' });
}
@action
toggleMask() {
this.showValue = !this.showValue;
}
}

View File

@@ -13,9 +13,9 @@
Choose a file… Choose a file…
</label> </label>
<span class="file-name has-text-grey-dark" data-test-text-file-input-label={{true}}> <span class="file-name has-text-grey-dark" data-test-text-file-input-label={{true}}>
{{or this.fileName "No file chosen"}} {{or this.filename "No file chosen"}}
</span> </span>
{{#if this.fileName}} {{#if this.filename}}
<button <button
type="button" type="button"
class="file-delete-button" class="file-delete-button"
@@ -29,7 +29,7 @@
</div> </div>
</div> </div>
</div> </div>
{{#if this.fileName}} {{#if this.filename}}
<p class="help has-text-grey"> <p class="help has-text-grey">
This file is This file is
{{this.fileSize}} {{this.fileSize}}

View File

@@ -58,13 +58,13 @@
<Icon @name="file" /> <Icon @name="file" />
</span> </span>
<label for="file-input" class="file-label has-text-grey-dark" data-test-pgp-file-input-label={{true}}> <label for="file-input" class="file-label has-text-grey-dark" data-test-pgp-file-input-label={{true}}>
{{#if this.key.fileName}} {{#if this.key.filename}}
{{this.key.fileName}} {{this.key.filename}}
{{else}} {{else}}
Choose a file&hellip; Choose a file&hellip;
{{/if}} {{/if}}
</label> </label>
{{#if this.key.fileName}} {{#if this.key.filename}}
<button type="button" class="file-delete-button" {{action "clearKey"}} data-test-pgp-clear={{true}}> <button type="button" class="file-delete-button" {{action "clearKey"}} data-test-pgp-clear={{true}}>
<Icon @name="x" aria-label="Close" /> <Icon @name="x" aria-label="Close" />
</button> </button>

View File

@@ -1,100 +0,0 @@
{{#unless this.inputOnly}}
<div class="level is-mobile">
<div class="level-left">
<label class="is-label" data-test-text-label={{true}}>
{{#if this.label}}
{{this.label}}
{{#if @helpText}}
<InfoTooltip>
<span data-test-help-text>
{{@helpText}}
</span>
</InfoTooltip>
{{/if}}
{{else}}
File
{{/if}}
</label>
</div>
<div class="level-right">
<div class="control is-flex">
<input
data-test-text-toggle
id={{concat "useText-" this.elementId}}
type="checkbox"
name={{concat "useText-" this.elementId}}
class="switch is-rounded is-success is-small"
checked={{@file.enterAsText}}
{{on "change" (toggle "enterAsText" @file)}}
/>
<label for={{concat "useText-" this.elementId}}>
Enter as text
</label>
</div>
</div>
</div>
{{/unless}}
<div
class="field text-file box is-fullwidth is-marginless is-shadowless {{if this.inputOnly 'is-paddingless'}}"
data-test-component="text-file"
>
{{#if @file.enterAsText}}
<div class="control has-icon-right">
<textarea
class="textarea {{unless this.showValue 'masked-font'}}"
{{on "input" this.updateData}}
data-test-text-file-textarea={{true}}
>{{@file.value}}</textarea>
<button
{{on "click" this.toggleMask}}
type="button"
class="{{if (eq this.value '') 'has-text-grey'}} masked-input-toggle button {{if this.displayOnly 'is-compact'}}"
data-test-button
>
<Icon @name={{if this.showValue "eye" "eye-off"}} />
</button>
</div>
<p class="help has-text-grey">
{{this.textareaHelpText}}
</p>
{{else}}
<div class="control is-expanded">
<div class="file has-name is-fullwidth">
<div class="file-label" aria-label="Choose a file">
<Input
id="file-input"
class="file-input"
@type="file"
{{on "change" this.pickedFile}}
data-test-text-file-input={{true}}
/>
<label for="file-input" class="file-cta button">
<Icon @name="upload" class="has-light-grey-text" />
Choose a file…
</label>
<span class="file-name has-text-grey-dark" data-test-text-file-input-label={{true}}>
{{#if @file.fileName}}
{{@file.fileName}}
{{else}}
No file chosen
{{/if}}
</span>
{{#if @file.fileName}}
<button
type="button"
class="file-delete-button"
aria-label="Clear file selection"
{{on "click" this.clearFile}}
data-test-text-clear={{true}}
>
<Icon @name="x-circle" />
</button>
{{/if}}
</div>
</div>
</div>
<p class="help has-text-grey">
{{this.fileHelpText}}
</p>
{{/if}}
</div>

View File

@@ -125,12 +125,14 @@
/> />
{{else if (eq @attr.options.editType "file")}} {{else if (eq @attr.options.editType "file")}}
{{! File Input }} {{! File Input }}
<TextFile <div class="has-bottom-margin-m">
@helpText={{@attr.options.helpText}} <TextFile
@file={{this.file}} @label={{this.labelString}}
@onChange={{this.setFile}} @helpText={{@attr.options.helpText}}
@label={{this.labelString}} @onChange={{this.setFile}}
/> @validationError={{this.validationError}}
/>
</div>
{{else if (eq @attr.options.editType "ttl")}} {{else if (eq @attr.options.editType "ttl")}}
{{! TTL Picker }} {{! TTL Picker }}
<div class="field"> <div class="field">
@@ -302,7 +304,7 @@
value={{or (get @model this.valuePath) @attr.options.defaultValue}} value={{or (get @model this.valuePath) @attr.options.defaultValue}}
onchange={{this.onChangeWithEvent}} onchange={{this.onChangeWithEvent}}
onkeyup={{this.handleKeyUp}} onkeyup={{this.handleKeyUp}}
class="input {{if this.validationError 'has-error-border'}}" class="input {{if this.validationError 'has-error-border'}} has-bottom-margin-m"
maxLength={{@attr.options.characterLimit}} maxLength={{@attr.options.characterLimit}}
/> />
{{#if @attr.options.validationAttr}} {{#if @attr.options.validationAttr}}

View File

@@ -56,7 +56,6 @@ export default class FormFieldComponent extends Component {
'ttl', 'ttl',
]; ];
@tracked showInput = false; @tracked showInput = false;
@tracked file = { value: '' }; // used by the pgp-file component when an attr is editType of 'file'
constructor() { constructor() {
super(...arguments); super(...arguments);
@@ -116,12 +115,11 @@ export default class FormFieldComponent extends Component {
} }
@action @action
setFile(_, keyFile) { setFile(keyFile) {
const path = this.valuePath; const path = this.valuePath;
const { value } = keyFile; const { value } = keyFile;
this.args.model.set(path, value); this.args.model.set(path, value);
this.onChange(path, value); this.onChange(path, value);
this.file = keyFile;
} }
@action @action
setAndBroadcast(value) { setAndBroadcast(value) {

View File

@@ -0,0 +1,95 @@
{{#unless @uploadOnly}}
<div class="level is-mobile">
<div class="level-left">
<label for="input-{{this.elementId}}" class="is-label" data-test-text-file-label>
{{or @label "File"}}
{{#if @helpText}}
<InfoTooltip>
<span data-test-help-text>
{{@helpText}}
</span>
</InfoTooltip>
{{/if}}
</label>
</div>
<div class="level-right">
<div class="control is-flex">
<Input
data-test-text-toggle
id="use-text-{{this.elementId}}"
class="switch is-rounded is-success is-small"
@type="checkbox"
@checked={{this.showTextArea}}
{{on "change" (fn (mut this.showTextArea) (not this.showTextArea))}}
/>
<label for="use-text-{{this.elementId}}">
Enter as text
</label>
</div>
</div>
</div>
{{/unless}}
<div class="field text-file box is-fullwidth is-marginless is-shadowless is-paddingless" data-test-component="text-file">
{{#if this.showTextArea}}
<div class="control has-icon-right">
<textarea
id="input-{{this.elementId}}"
class="textarea {{if (and (not this.showValue) this.content) 'masked-font'}}"
{{on "input" this.handleTextInput}}
data-test-text-file-textarea
>
{{this.content}}
</textarea>
<button
type="button"
class="masked-input-toggle button is-compact"
data-test-button="toggle-masked"
{{on "click" (fn (mut this.showValue) (not this.showValue))}}
>
<Icon @name={{if this.showValue "eye" "eye-off"}} @stretched={{true}} />
</button>
</div>
<p class="help has-text-grey">Enter the value as text</p>
{{else}}
<div class="control is-expanded">
<div class="file has-name is-fullwidth">
<div class="file-label" aria-label="Choose a file">
<Input
id="file-input-{{this.elementId}}"
class="file-input"
@type="file"
{{on "change" this.handleFileUpload}}
data-test-text-file-input
/>
<label for="file-input-{{this.elementId}}" class="file-cta button">
<Icon @name="upload" class="has-light-grey-text" />
Choose a file…
</label>
<span class="file-name has-text-grey-dark" data-test-text-file-input-label>
{{or this.filename "No file chosen"}}
</span>
{{#if this.filename}}
<button
type="button"
class="file-delete-button"
aria-label="Clear file selection"
{{on "click" this.clearFile}}
data-test-text-clear
>
<Icon @name="x-circle" />
</button>
{{/if}}
</div>
</div>
</div>
<p class="help has-text-grey">Select a file from your computer</p>
{{#if (or @validationError this.uploadError)}}
<AlertInline
@type="danger"
@message={{or @validationError this.uploadError}}
@paddingTop={{true}}
data-test-field-validation="text-file"
/>
{{/if}}
{{/if}}
</div>

View File

@@ -0,0 +1,69 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { guidFor } from '@ember/object/internals';
/**
* @module TextFile
* `TextFile` components render a file upload input with the option to toggle a "Enter as text" button
* that changes the input into a textarea
*
* @example
* <TextFile
* @uploadOnly={{true}}
* @helpText="help text"
* @onChange={{this.handleChange}}
* @label="PEM Bundle"
* />
*
* @param {function} onChange - Callback function to call when the value of the input changes, returns an object in the shape of { value: fileContents, filename: 'some-file.txt' }
* @param {bool} [uploadOnly=false] - When true, renders a static file upload input and removes the option to toggle and input plain text
* @param {string} [helpText] - Text underneath label.
* @param {string} [label='File'] - Text to use as the label for the file input. If none, default of 'File' is rendered
*/
export default class TextFileComponent extends Component {
@tracked content = '';
@tracked filename = '';
@tracked uploadError = '';
@tracked showValue = false;
@tracked showTextArea = false;
elementId = guidFor(this);
async readFile(file) {
try {
this.content = await file.text();
this.filename = file.name;
this.handleChange();
} catch (error) {
this.clearFile();
this.uploadError = 'There was a problem uploading. Please try again.';
}
}
@action
handleFileUpload(e) {
e.preventDefault();
const { files } = e.target;
if (!files.length) return;
this.readFile(files[0]);
}
@action
handleTextInput(e) {
e.preventDefault();
this.content = e.target.value;
this.handleChange();
}
@action
clearFile() {
this.content = '';
this.filename = '';
this.handleChange();
}
handleChange() {
this.args.onChange({ value: this.content, filename: this.filename });
this.uploadError = '';
}
}

View File

@@ -1,5 +1,5 @@
{{#if this.isFlightIcon}} {{#if this.isFlightIcon}}
<FlightIcon @name={{@name}} @size={{@size}} ...attributes /> <FlightIcon @name={{@name}} @size={{@size}} @stretched={{@stretched}} ...attributes />
{{else}} {{else}}
<span class="hs-icon {{this.hsIconClass}}" ...attributes> <span class="hs-icon {{this.hsIconClass}}" ...attributes>
{{svg-jar @name}} {{svg-jar @name}}

View File

@@ -47,7 +47,7 @@
onclick={{action "toggleMask"}} onclick={{action "toggleMask"}}
type="button" type="button"
class="{{if (eq this.value '') 'has-text-grey'}} masked-input-toggle button" class="{{if (eq this.value '') 'has-text-grey'}} masked-input-toggle button"
data-test-button data-test-button="toggle-masked"
> >
<Icon @name={{if this.showValue "eye" "eye-off"}} /> <Icon @name={{if this.showValue "eye" "eye-off"}} />
</button> </button>

View File

@@ -107,7 +107,7 @@
<div class="message-body"> <div class="message-body">
<h4 class="title is-7 is-marginless"> <h4 class="title is-7 is-marginless">
PGP Key PGP Key
{{this.pgpKeyFile.fileName}} {{this.pgpKeyFile.filename}}
</h4> </h4>
<code class="is-word-break">{{this.pgp_key}}</code> <code class="is-word-break">{{this.pgp_key}}</code>
</div> </div>

View File

@@ -129,7 +129,7 @@
</p> </p>
<h4 class="field-title has-bottom-padding-m is-fullwidth"> <h4 class="field-title has-bottom-padding-m is-fullwidth">
PGP Key PGP Key
{{this.pgpKeyFile.fileName}} {{this.pgpKeyFile.filename}}
</h4> </h4>
<div class="message is-list has-copy-button" tabindex="-1"> <div class="message is-list has-copy-button" tabindex="-1">
<HoverCopyButton @copyValue={{this.pgp_key}} /> <HoverCopyButton @copyValue={{this.pgp_key}} />

View File

@@ -0,0 +1 @@
export { default } from 'core/components/text-file';

View File

@@ -105,7 +105,7 @@ module('Integration | Component | form field', function (hooks) {
await click('[data-test-text-toggle]'); await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', 'hello world'); await fillIn('[data-test-text-file-textarea]', 'hello world');
assert.dom('[data-test-text-file-textarea]').hasClass('masked-font'); assert.dom('[data-test-text-file-textarea]').hasClass('masked-font');
await click('[data-test-button]'); await click('[data-test-button="toggle-masked"]');
assert.dom('[data-test-text-file-textarea]').doesNotHaveClass('masked-font'); assert.dom('[data-test-text-file-textarea]').doesNotHaveClass('masked-font');
}); });

View File

@@ -1,8 +1,8 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { click, fillIn, render } from '@ember/test-helpers'; import { click, fillIn, render, triggerEvent } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import Sinon from 'sinon'; import sinon from 'sinon';
import Pretender from 'pretender'; import Pretender from 'pretender';
const SELECTORS = { const SELECTORS = {
@@ -21,8 +21,8 @@ module('Integration | Component | policy-form', function (hooks) {
hooks.beforeEach(function () { hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store'); this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('policy/acl'); this.model = this.store.createRecord('policy/acl');
this.onSave = Sinon.spy(); this.onSave = sinon.spy();
this.onCancel = Sinon.spy(); this.onCancel = sinon.spy();
this.server = new Pretender(function () { this.server = new Pretender(function () {
this.put('/v1/sys/policies/acl/bad-policy', () => { this.put('/v1/sys/policies/acl/bad-policy', () => {
return [ return [
@@ -44,15 +44,11 @@ module('Integration | Component | policy-form', function (hooks) {
}); });
test('it renders the form for new ACL policy', async function (assert) { test('it renders the form for new ACL policy', async function (assert) {
const saveSpy = Sinon.spy();
const model = this.store.createRecord('policy/acl');
const policy = ` const policy = `
path "secret/*" { path "secret/*" {
capabilities = [ "create", "read", "update", "list" ] capabilities = [ "create", "read", "update", "list" ]
} }
`; `;
this.set('model', model);
this.set('onSave', saveSpy);
await render(hbs` await render(hbs`
<PolicyForm <PolicyForm
@model={{this.model}} @model={{this.model}}
@@ -67,14 +63,13 @@ module('Integration | Component | policy-form', function (hooks) {
assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input'); assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input');
await fillIn(`${SELECTORS.policyEditor} textarea`, policy); await fillIn(`${SELECTORS.policyEditor} textarea`, policy);
assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model'); assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model');
assert.ok(saveSpy.notCalled); assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.saveButton).hasText('Create policy'); assert.dom(SELECTORS.saveButton).hasText('Create policy');
await click(SELECTORS.saveButton); await click(SELECTORS.saveButton);
assert.ok(saveSpy.calledOnceWith(this.model)); assert.ok(this.onSave.calledOnceWith(this.model));
}); });
test('it renders the form for new RGP policy', async function (assert) { test('it renders the form for new RGP policy', async function (assert) {
const saveSpy = Sinon.spy();
const model = this.store.createRecord('policy/rgp'); const model = this.store.createRecord('policy/rgp');
const policy = ` const policy = `
path "secret/*" { path "secret/*" {
@@ -82,7 +77,6 @@ module('Integration | Component | policy-form', function (hooks) {
} }
`; `;
this.set('model', model); this.set('model', model);
this.set('onSave', saveSpy);
await render(hbs` await render(hbs`
<PolicyForm <PolicyForm
@model={{this.model}} @model={{this.model}}
@@ -97,13 +91,18 @@ module('Integration | Component | policy-form', function (hooks) {
assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input'); assert.strictEqual(this.model.name, 'foo', 'Input sets name on model to lowercase input');
await fillIn(`${SELECTORS.policyEditor} textarea`, policy); await fillIn(`${SELECTORS.policyEditor} textarea`, policy);
assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model'); assert.strictEqual(this.model.policy, policy, 'Policy editor sets policy on model');
assert.ok(saveSpy.notCalled); assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.saveButton).hasText('Create policy'); assert.dom(SELECTORS.saveButton).hasText('Create policy');
await click(SELECTORS.saveButton); await click(SELECTORS.saveButton);
assert.ok(saveSpy.calledOnceWith(this.model)); assert.ok(this.onSave.calledOnceWith(this.model));
}); });
test('it toggles upload on new policy', async function (assert) { test('it toggles to upload a new policy and uploads file', async function (assert) {
const policy = `
path "auth/token/lookup-self" {
capabilities = ["read"]
}`;
this.file = new File([policy], 'test-policy.hcl');
await render(hbs` await render(hbs`
<PolicyForm <PolicyForm
@model={{this.model}} @model={{this.model}}
@@ -117,10 +116,13 @@ module('Integration | Component | policy-form', function (hooks) {
await click(SELECTORS.uploadFileToggle); await click(SELECTORS.uploadFileToggle);
assert.dom(SELECTORS.policyUpload).exists({ count: 1 }, 'Policy upload is shown after toggle'); assert.dom(SELECTORS.policyUpload).exists({ count: 1 }, 'Policy upload is shown after toggle');
assert.dom(SELECTORS.policyEditor).doesNotExist('Policy editor is not shown'); assert.dom(SELECTORS.policyEditor).doesNotExist('Policy editor is not shown');
await triggerEvent(SELECTORS.policyUpload, 'change', { files: [this.file] });
assert.dom(SELECTORS.nameInput).hasValue('test-policy', 'it fills in policy name');
await click(SELECTORS.saveButton);
assert.propEqual(this.onSave.lastCall.args[0].policy, policy, 'policy content saves in correct format');
}); });
test('it renders the form to edit existing ACL policy', async function (assert) { test('it renders the form to edit existing ACL policy', async function (assert) {
const saveSpy = Sinon.spy();
const model = this.store.createRecord('policy/acl', { const model = this.store.createRecord('policy/acl', {
name: 'bar', name: 'bar',
policy: 'some policy content', policy: 'some policy content',
@@ -128,7 +130,6 @@ module('Integration | Component | policy-form', function (hooks) {
model.save(); model.save();
this.set('model', model); this.set('model', model);
this.set('onSave', saveSpy);
await render(hbs` await render(hbs`
<PolicyForm <PolicyForm
@model={{this.model}} @model={{this.model}}
@@ -145,13 +146,12 @@ module('Integration | Component | policy-form', function (hooks) {
'updated-some policy content', 'updated-some policy content',
'Policy editor updates policy value on model' 'Policy editor updates policy value on model'
); );
assert.ok(saveSpy.notCalled); assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.saveButton).hasText('Save', 'Save button text is correct'); assert.dom(SELECTORS.saveButton).hasText('Save', 'Save button text is correct');
await click(SELECTORS.saveButton); await click(SELECTORS.saveButton);
assert.ok(saveSpy.calledOnceWith(this.model)); assert.ok(this.onSave.calledOnceWith(this.model));
}); });
test('it renders the form to edit existing RGP policy', async function (assert) { test('it renders the form to edit existing RGP policy', async function (assert) {
const saveSpy = Sinon.spy();
const model = this.store.createRecord('policy/rgp', { const model = this.store.createRecord('policy/rgp', {
name: 'bar', name: 'bar',
policy: 'some policy content', policy: 'some policy content',
@@ -159,7 +159,6 @@ module('Integration | Component | policy-form', function (hooks) {
model.save(); model.save();
this.set('model', model); this.set('model', model);
this.set('onSave', saveSpy);
await render(hbs` await render(hbs`
<PolicyForm <PolicyForm
@model={{this.model}} @model={{this.model}}
@@ -176,20 +175,18 @@ module('Integration | Component | policy-form', function (hooks) {
'updated-some policy content', 'updated-some policy content',
'Policy editor updates policy value on model' 'Policy editor updates policy value on model'
); );
assert.ok(saveSpy.notCalled); assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.saveButton).hasText('Save', 'Save button text is correct'); assert.dom(SELECTORS.saveButton).hasText('Save', 'Save button text is correct');
await click(SELECTORS.saveButton); await click(SELECTORS.saveButton);
assert.ok(saveSpy.calledOnceWith(this.model)); assert.ok(this.onSave.calledOnceWith(this.model));
}); });
test('it shows the error message on form when save fails', async function (assert) { test('it shows the error message on form when save fails', async function (assert) {
const saveSpy = Sinon.spy();
const model = this.store.createRecord('policy/acl', { const model = this.store.createRecord('policy/acl', {
name: 'bad-policy', name: 'bad-policy',
policy: 'some policy content', policy: 'some policy content',
}); });
this.set('model', model); this.set('model', model);
this.set('onSave', saveSpy);
await render(hbs` await render(hbs`
<PolicyForm <PolicyForm
@model={{this.model}} @model={{this.model}}
@@ -198,7 +195,7 @@ module('Integration | Component | policy-form', function (hooks) {
/> />
`); `);
await click(SELECTORS.saveButton); await click(SELECTORS.saveButton);
assert.ok(saveSpy.notCalled); assert.ok(this.onSave.notCalled);
assert.dom(SELECTORS.error).includesText('An error occurred'); assert.dom(SELECTORS.error).includesText('An error occurred');
}); });
}); });

View File

@@ -0,0 +1,113 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, fillIn, render, triggerEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
const SELECTORS = {
label: '[data-test-text-file-label]',
toggle: '[data-test-text-toggle]',
textarea: '[data-test-text-file-textarea]',
fileUpload: '[data-test-text-file-input]',
};
module('Integration | Component | text-file', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.label = 'Some label';
this.onChange = sinon.spy();
this.owner.lookup('service:flash-messages').registerTypes(['danger']);
});
test('it renders with label and toggle by default', async function (assert) {
await render(hbs`<TextFile @onChange={{this.onChange}} />`);
assert.dom(SELECTORS.label).hasText('File', 'renders default label');
assert.dom(SELECTORS.toggle).exists({ count: 1 }, 'toggle exists');
assert.dom(SELECTORS.fileUpload).exists({ count: 1 }, 'File input shown');
});
test('it renders without toggle and option for text input when uploadOnly=true', async function (assert) {
await render(hbs`<TextFile @onChange={{this.onChange}} @uploadOnly={{true}} />`);
assert.dom(SELECTORS.label).doesNotExist('Label no longer rendered');
assert.dom(SELECTORS.toggle).doesNotExist('toggle no longer rendered');
assert.dom(SELECTORS.fileUpload).exists({ count: 1 }, 'File input shown');
});
test('it toggles between upload and textarea', async function (assert) {
await render(hbs`<TextFile @onChange={{this.onChange}} />`);
assert.dom(SELECTORS.fileUpload).exists({ count: 1 }, 'File input shown');
assert.dom(SELECTORS.textarea).doesNotExist('Texarea hidden');
await click(SELECTORS.toggle);
assert.dom(SELECTORS.textarea).exists({ count: 1 }, 'Textarea shown');
assert.dom(SELECTORS.fileUpload).doesNotExist('File upload hidden');
});
test('it correctly parses uploaded files', async function (assert) {
this.file = new File(['some content for a file'], 'filename.txt');
await render(hbs`<TextFile @onChange={{this.onChange}} />`);
await triggerEvent(SELECTORS.fileUpload, 'change', { files: [this.file] });
assert.propEqual(
this.onChange.lastCall.args[0],
{
filename: 'filename.txt',
value: 'some content for a file',
},
'parent callback function is called with correct arguments'
);
});
test('it correctly submits text input', async function (assert) {
const PEM_BUNDLE = `-----BEGIN CERTIFICATE-----
MIIDGjCCAgKgAwIBAgIUFvnhb2nQ8+KNS3SzjlfYDMHGIRgwDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEAxMCZmEwHhcNMTgwMTEwMTg1NDI5WhcNMTgwMjExMTg1NDU5
WjANMQswCQYDVQQDEwJmYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN2VtBn6EMlA4aYre/xoKHxlgNDxJnfSQWfs6yF/K201qPnt4QF9AXChatbmcKVn
OaURq+XEJrGVgF/u2lSos3NRZdhWVe8o3/sOetsGxcrd0gXAieOSmkqJjp27bYdl
uY3WsxhyiPvdfS6xz39OehsK/YCB6qCzwB4eEfSKqbkvfDL9sLlAiOlaoHC9pczf
6/FANKp35UDwInSwmq5vxGbnWk9zMkh5Jq6hjOWHZnVc2J8J49PYvkIM8uiHDgOE
w71T2xM5plz6crmZnxPCOcTKIdF7NTEP2lUfiqc9lONV9X1Pi4UclLPHJf5bwTmn
JaWgbKeY+IlF61/mgxzhC7cCAwEAAaNyMHAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
EwEB/wQFMAMBAf8wHQYDVR0OBBYEFLDtc6+HZN2lv60JSDAZq3+IHoq7MB8GA1Ud
IwQYMBaAFLDtc6+HZN2lv60JSDAZq3+IHoq7MA0GA1UdEQQGMASCAmZhMA0GCSqG
SIb3DQEBCwUAA4IBAQDVt6OddTV1MB0UvF5v4zL1bEB9bgXvWx35v/FdS+VGn/QP
cC2c4ZNukndyHhysUEPdqVg4+up1aXm4eKXzNmGMY/ottN2pEhVEWQyoIIA1tH0e
8Kv/bysYpHZKZuoGg5+mdlHS2p2Dh2bmYFyBLJ8vaeP83NpTs2cNHcmEvWh/D4UN
UmYDODRN4qh9xYruKJ8i89iMGQfbdcq78dCC4JwBIx3bysC8oF4lqbTYoYNVTnAi
LVqvLdHycEOMlqV0ecq8uMLhPVBalCmIlKdWNQFpXB0TQCsn95rCCdi7ZTsYk5zv
Q4raFvQrZth3Cz/X5yPTtQL78oBYrmHzoQKDFJ2z
-----END CERTIFICATE-----`;
await render(hbs`<TextFile @onChange={{this.onChange}} />`);
await click(SELECTORS.toggle);
await fillIn(SELECTORS.textarea, PEM_BUNDLE);
assert.propEqual(
this.onChange.lastCall.args[0],
{
filename: '',
value: PEM_BUNDLE,
},
'parent callback function is called with correct text area input'
);
});
test('it throws an error when it cannot read the file', async function (assert) {
this.file = { foo: 'bar' };
await render(hbs`<TextFile @onChange={{this.onChange}} />`);
await triggerEvent(SELECTORS.fileUpload, 'change', { files: [this.file] });
assert
.dom('[data-test-field-validation="text-file"]')
.hasText('There was a problem uploading. Please try again.');
assert.propEqual(
this.onChange.lastCall.args[0],
{
filename: '',
value: '',
},
'parent callback function is called with cleared out values'
);
});
});

View File

@@ -3,5 +3,5 @@ import { clickable, isPresent } from 'ember-cli-page-object';
export default { export default {
textareaIsPresent: isPresent('[data-test-textarea]'), textareaIsPresent: isPresent('[data-test-textarea]'),
copyButtonIsPresent: isPresent('[data-test-copy-button]'), copyButtonIsPresent: isPresent('[data-test-copy-button]'),
toggleMasked: clickable('[data-test-button]'), toggleMasked: clickable('[data-test-button="toggle-masked"]'),
}; };