UI/Wrong sentinel error message for auth methods (#14551)
* priortize adapter error over model error * glimmerize message-error component * message error tweaks * fix glimmerize * fix some tests * change error handling for mount backend form * throw API error for secret engine not mounting * fix tests" * fix tests * cleanup error handling for secret engine mounts * fix test selector * add changelog * STOP BEING FLAKY
This commit is contained in:
parent
2615668aad
commit
fbce5986c1
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: Fix issue where UI incorrectly handled API errors when mounting backends
|
||||||
|
```
|
|
@ -61,15 +61,8 @@ export default ApplicationAdapter.extend({
|
||||||
data.id = path;
|
data.id = path;
|
||||||
}
|
}
|
||||||
// first create the engine
|
// first create the engine
|
||||||
try {
|
await this.ajax(this.url(path), 'POST', { data });
|
||||||
await this.ajax(this.url(path), 'POST', { data });
|
|
||||||
} catch (e) {
|
|
||||||
// if error determine if path duplicate or permissions
|
|
||||||
if (e.httpStatus === 400) {
|
|
||||||
throw new Error('samePath');
|
|
||||||
}
|
|
||||||
throw new Error('mountIssue');
|
|
||||||
}
|
|
||||||
// second post to config
|
// second post to config
|
||||||
try {
|
try {
|
||||||
await this.ajax(this.urlForConfig(path), 'POST', { data: configData });
|
await this.ajax(this.urlForConfig(path), 'POST', { data: configData });
|
||||||
|
|
|
@ -7,6 +7,7 @@ export default Component.extend({
|
||||||
classNames: 'config-pki-ca',
|
classNames: 'config-pki-ca',
|
||||||
store: service('store'),
|
store: service('store'),
|
||||||
flashMessages: service(),
|
flashMessages: service(),
|
||||||
|
errors: null,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @param boolean
|
* @param boolean
|
||||||
|
@ -150,8 +151,8 @@ export default Component.extend({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
// handle promise rejection - error will be shown by message-error component
|
this.set('errors', e.errors);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.set('loading', false);
|
this.set('loading', false);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { get } from '@ember/object';
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
classNames: 'config-pki',
|
classNames: 'config-pki',
|
||||||
flashMessages: service(),
|
flashMessages: service(),
|
||||||
|
errors: null,
|
||||||
/*
|
/*
|
||||||
*
|
*
|
||||||
* @param String
|
* @param String
|
||||||
|
@ -55,8 +55,8 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
this.send('refresh');
|
this.send('refresh');
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
// handle promise rejection - error will be shown by message-error component
|
this.set('errors', e.errors);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.set('loading', false);
|
this.set('loading', false);
|
||||||
|
|
|
@ -113,8 +113,39 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!capabilities.get('canUpdate')) {
|
let changedAttrKeys = Object.keys(mountModel.changedAttributes());
|
||||||
// if there is no sys/mount issue then error is config endpoint.
|
const updatesConfig =
|
||||||
|
mountModel.isV2KV &&
|
||||||
|
(changedAttrKeys.includes('casRequired') ||
|
||||||
|
changedAttrKeys.includes('deleteVersionAfter') ||
|
||||||
|
changedAttrKeys.includes('maxVersions'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield mountModel.save();
|
||||||
|
} catch (err) {
|
||||||
|
if (err.httpStatus === 403) {
|
||||||
|
this.mountIssue = true;
|
||||||
|
this.set('isFormInvalid', this.mountIssue);
|
||||||
|
this.flashMessages.danger(
|
||||||
|
'You do not have access to the sys/mounts endpoint. The secret engine was not mounted.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (err.errors) {
|
||||||
|
let errors = err.errors.map((e) => {
|
||||||
|
if (typeof e === 'object') return e.title || e.message || JSON.stringify(e);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
this.set('errors', errors);
|
||||||
|
} else if (err.message) {
|
||||||
|
this.set('errorMessage', err.message);
|
||||||
|
} else {
|
||||||
|
this.set('errorMessage', 'An error occurred, check the vault logs.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (updatesConfig && !capabilities.get('canUpdate')) {
|
||||||
|
// config error is not thrown from secret-engine adapter, so handling here
|
||||||
this.flashMessages.warning(
|
this.flashMessages.warning(
|
||||||
'You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.'
|
'You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.'
|
||||||
);
|
);
|
||||||
|
@ -125,20 +156,6 @@ export default Component.extend({
|
||||||
0,
|
0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
yield mountModel.save();
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message === 'mountIssue') {
|
|
||||||
this.mountIssue = true;
|
|
||||||
this.set('isFormInvalid', this.mountIssue);
|
|
||||||
this.flashMessages.danger(
|
|
||||||
'You do not have access to the sys/mounts endpoint. The secret engine was not mounted.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.set('errorMessage', 'This mount path already exist.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mountType = this.mountType;
|
let mountType = this.mountType;
|
||||||
mountType = mountType === 'secret' ? `${mountType}s engine` : `${mountType} method`;
|
mountType = mountType === 'secret' ? `${mountType}s engine` : `${mountType} method`;
|
||||||
this.flashMessages.success(`Successfully mounted the ${type} ${mountType} at ${path}.`);
|
this.flashMessages.success(`Successfully mounted the ${type} ${mountType} at ${path}.`);
|
||||||
|
|
|
@ -36,9 +36,8 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
return this.transitionToRoute('vault.cluster.policy.show', m.get('policyType'), m.get('name'));
|
return this.transitionToRoute('vault.cluster.policy.show', m.get('policyType'), m.get('name'));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
// do nothing here...
|
model.set('errors', e.errors);
|
||||||
// message-error component will show errors
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ import { computed } from '@ember/object';
|
||||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||||
|
|
||||||
export default Model.extend({
|
export default Model.extend({
|
||||||
|
errors: attr('array'),
|
||||||
name: attr('string'),
|
name: attr('string'),
|
||||||
policy: attr('string'),
|
policy: attr('string'),
|
||||||
policyType: computed('constructor.modelName', function () {
|
policyType: computed('constructor.modelName', function () {
|
||||||
return this.constructor.modelName.split('/')[1];
|
return this.constructor.modelName.split('/')[1];
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updatePath: lazyCapabilities(apiPath`sys/policies/${'policyType'}/${'id'}`, 'id', 'policyType'),
|
updatePath: lazyCapabilities(apiPath`sys/policies/${'policyType'}/${'id'}`, 'id', 'policyType'),
|
||||||
canDelete: alias('updatePath.canDelete'),
|
canDelete: alias('updatePath.canDelete'),
|
||||||
canEdit: alias('updatePath.canUpdate'),
|
canEdit: alias('updatePath.canUpdate'),
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<h2 data-test-title class="title is-3">Sign intermediate</h2>
|
<h2 data-test-title class="title is-3">Sign intermediate</h2>
|
||||||
<NamespaceReminder @mode="save" @noun="PKI change" />
|
<NamespaceReminder @mode="save" @noun="PKI change" />
|
||||||
<MessageError @model={{this.model}} />
|
<MessageError @model={{this.model}} @errors={{this.errors}} />
|
||||||
<form {{action "saveCA" on="submit"}} data-test-sign-intermediate-form="true">
|
<form {{action "saveCA" on="submit"}} data-test-sign-intermediate-form="true">
|
||||||
<FormFieldGroupsLoop @model={{this.model}} @mode={{this.mode}} />
|
<FormFieldGroupsLoop @model={{this.model}} @mode={{this.mode}} />
|
||||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<MessageError @model={{this.config}} />
|
<MessageError @model={{this.config}} @errors={{this.errors}} />
|
||||||
<form {{action "save" this.section on="submit"}} class="box is-shadowless is-marginless is-fullwidth has-slim-padding">
|
<form {{action "save" this.section on="submit"}} class="box is-shadowless is-marginless is-fullwidth has-slim-padding">
|
||||||
{{#each (get this.config (concat this.section "Attrs")) as |attr|}}
|
{{#each (get this.config (concat this.section "Attrs")) as |attr|}}
|
||||||
<FormField @attr={{attr}} @model={{this.config}} @data-test-field={{attr.name}} />
|
<FormField @attr={{attr}} @model={{this.config}} @data-test-field={{attr.name}} />
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<form {{action (perform this.mountBackend) on="submit"}}>
|
<form {{action (perform this.mountBackend) on="submit"}}>
|
||||||
<div class="box is-sideless is-fullwidth is-marginless">
|
<div class="box is-sideless is-fullwidth is-marginless">
|
||||||
<NamespaceReminder @mode="enable" @noun={{if (eq this.mountType "auth") "Auth Method" "Secret Engine"}} />
|
<NamespaceReminder @mode="enable" @noun={{if (eq this.mountType "auth") "Auth Method" "Secret Engine"}} />
|
||||||
<MessageError @model={{this.model}} @errorMessage={{this.errorMessage}} />
|
<MessageError @model={{this.model}} @errorMessage={{this.errorMessage}} @errors={{this.errors}} />
|
||||||
{{#if this.showEnable}}
|
{{#if this.showEnable}}
|
||||||
<FormFieldGroups
|
<FormFieldGroups
|
||||||
@model={{this.mountModel}}
|
@model={{this.mountModel}}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<form {{action "savePolicy" this.model on="submit"}}>
|
<form {{action "savePolicy" this.model on="submit"}}>
|
||||||
<div class="box is-bottomless is-fullwidth is-marginless">
|
<div class="box is-bottomless is-fullwidth is-marginless">
|
||||||
<MessageError @model={{this.model}} />
|
<MessageError @model={{this.model}} @errors={{this.model.errors}} />
|
||||||
<NamespaceReminder @mode="create" @noun="policy" />
|
<NamespaceReminder @mode="create" @noun="policy" />
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="policy-name" class="is-label">Name</label>
|
<label for="policy-name" class="is-label">Name</label>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { computed } from '@ember/object';
|
import Component from '@glimmer/component';
|
||||||
import Component from '@ember/component';
|
|
||||||
import layout from '../templates/components/message-error';
|
import layout from '../templates/components/message-error';
|
||||||
|
import { setComponentTemplate } from '@ember/component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module MessageError
|
* @module MessageError
|
||||||
|
@ -11,48 +11,37 @@ import layout from '../templates/components/message-error';
|
||||||
* <MessageError @model={{model}} />
|
* <MessageError @model={{model}} />
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param model=null{DS.Model} - An Ember data model that will be used to bind error statest to the internal
|
* @param {object} [model=null] - An Ember data model that will be used to bind error states to the internal
|
||||||
* `errors` property.
|
* `errors` property.
|
||||||
* @param errors=null{Array} - An array of error strings to show.
|
* @param {array} [errors=null] - An array of error strings to show.
|
||||||
* @param errorMessage=null{String} - An Error string to display.
|
* @param {string} [errorMessage=null] - An Error string to display.
|
||||||
*/
|
*/
|
||||||
export default Component.extend({
|
|
||||||
layout,
|
|
||||||
model: null,
|
|
||||||
errors: null,
|
|
||||||
errorMessage: null,
|
|
||||||
|
|
||||||
displayErrors: computed(
|
class MessageError extends Component {
|
||||||
'errorMessage',
|
get displayErrors() {
|
||||||
'model.{isError,adapterError.message,adapterError.errors.@each}',
|
let { errorMessage, errors, model } = this.args;
|
||||||
'errors',
|
if (errorMessage) {
|
||||||
'errors.[]',
|
return [errorMessage];
|
||||||
function () {
|
|
||||||
const errorMessage = this.errorMessage;
|
|
||||||
const errors = this.errors;
|
|
||||||
const modelIsError = this.model?.isError;
|
|
||||||
if (errorMessage) {
|
|
||||||
return [errorMessage];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelIsError) {
|
|
||||||
if (!this.model.adapterError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.model.adapterError.errors.length > 0) {
|
|
||||||
return this.model.adapterError.errors.map((e) => {
|
|
||||||
if (typeof e === 'object') return e.title || e.message || JSON.stringify(e);
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [this.model.adapterError.message];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'no error';
|
|
||||||
}
|
}
|
||||||
),
|
|
||||||
});
|
if (errors && errors.length > 0) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model?.isError) {
|
||||||
|
let adapterError = model?.adapterError;
|
||||||
|
if (!adapterError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (adapterError.errors.length > 0) {
|
||||||
|
return adapterError.errors.map((e) => {
|
||||||
|
if (typeof e === 'object') return e.title || e.message || JSON.stringify(e);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [adapterError.message];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default setComponentTemplate(layout, MessageError);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{#if this.displayErrors.length}}
|
{{#if this.displayErrors}}
|
||||||
{{#each this.displayErrors as |error|}}
|
{{#each this.displayErrors as |error|}}
|
||||||
<AlertBanner @type="danger" @message={{error}} data-test-error />
|
<AlertBanner @type="danger" @message={{error}} data-test-error ...attributes />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
|
@ -112,9 +112,8 @@ module('Acceptance | clients current', function (hooks) {
|
||||||
// FILTER BY NAMESPACE
|
// FILTER BY NAMESPACE
|
||||||
await clickTrigger();
|
await clickTrigger();
|
||||||
await searchSelect.options.objectAt(0).click();
|
await searchSelect.options.objectAt(0).click();
|
||||||
await waitUntil(() => {
|
await settled();
|
||||||
return find('[data-test-horizontal-bar-chart]');
|
await waitUntil(() => find('[data-test-horizontal-bar-chart]'));
|
||||||
});
|
|
||||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
||||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
||||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
||||||
|
@ -145,18 +144,14 @@ module('Acceptance | clients current', function (hooks) {
|
||||||
// FILTER BY AUTH METHOD
|
// FILTER BY AUTH METHOD
|
||||||
await clickTrigger();
|
await clickTrigger();
|
||||||
await searchSelect.options.objectAt(0).click();
|
await searchSelect.options.objectAt(0).click();
|
||||||
await waitUntil(() => {
|
await waitUntil(() => find('#auth-method-search-select'));
|
||||||
return find('#auth-method-search-select');
|
|
||||||
});
|
|
||||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('5');
|
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('5');
|
||||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('3');
|
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('3');
|
||||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('2');
|
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('2');
|
||||||
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
|
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
|
||||||
// Delete auth filter goes back to filtered only on namespace
|
// Delete auth filter goes back to filtered only on namespace
|
||||||
await click('#auth-method-search-select [data-test-selected-list-button="delete"]');
|
await click('#auth-method-search-select [data-test-selected-list-button="delete"]');
|
||||||
await waitUntil(() => {
|
await waitUntil(() => find('[data-test-horizontal-bar-chart]'));
|
||||||
return find('[data-test-horizontal-bar-chart]');
|
|
||||||
});
|
|
||||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
||||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
||||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { currentRouteName, settled } from '@ember/test-helpers';
|
import { currentRouteName, settled, find, waitUntil } from '@ember/test-helpers';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section';
|
import page from 'vault/tests/pages/settings/configure-secret-backends/pki/section';
|
||||||
|
@ -22,8 +22,7 @@ module('Acceptance | settings/configure/secrets/pki/urls', function (hooks) {
|
||||||
|
|
||||||
await page.form.fields.objectAt(0).textarea('foo').change();
|
await page.form.fields.objectAt(0).textarea('foo').change();
|
||||||
await page.form.submit();
|
await page.form.submit();
|
||||||
await settled();
|
await waitUntil(() => find('[data-test-error]'));
|
||||||
|
|
||||||
assert.ok(page.form.hasError, 'shows error on invalid input');
|
assert.ok(page.form.hasError, 'shows error on invalid input');
|
||||||
|
|
||||||
await page.form.fields.objectAt(0).textarea('foo.example.com').change();
|
await page.form.fields.objectAt(0).textarea('foo.example.com').change();
|
||||||
|
|
|
@ -89,7 +89,7 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
|
||||||
await page.enableEngine();
|
await page.enableEngine();
|
||||||
await page.selectType('kv');
|
await page.selectType('kv');
|
||||||
await page.next().path(path).submit();
|
await page.next().path(path).submit();
|
||||||
assert.dom('.alert-banner-message-body').hasText('This mount path already exist.');
|
assert.dom('.alert-banner-message-body').containsText(`path is already in use at ${path}`);
|
||||||
assert.equal(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
|
assert.equal(currentRouteName(), 'vault.cluster.settings.mount-secret-backend');
|
||||||
|
|
||||||
await page.secretList();
|
await page.secretList();
|
||||||
|
|
|
@ -61,7 +61,7 @@ module('Integration | Component | auth form', function (hooks) {
|
||||||
this.set('cluster', EmberObject.create({ standby: true }));
|
this.set('cluster', EmberObject.create({ standby: true }));
|
||||||
this.set('selectedAuth', 'token');
|
this.set('selectedAuth', 'token');
|
||||||
await render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
|
await render(hbs`{{auth-form cluster=cluster selectedAuth=selectedAuth}}`);
|
||||||
assert.equal(component.errorText, '');
|
assert.equal(component.errorMessagePresent, false);
|
||||||
component.login();
|
component.login();
|
||||||
// because this is an ember-concurrency backed service,
|
// because this is an ember-concurrency backed service,
|
||||||
// we have to manually force settling the run queue
|
// we have to manually force settling the run queue
|
||||||
|
|
|
@ -17,11 +17,10 @@ module('Integration | Component | ttl picker', function (hooks) {
|
||||||
|
|
||||||
let callCount = this.changeSpy.callCount;
|
let callCount = this.changeSpy.callCount;
|
||||||
await fillIn('[data-test-ttl-value]', 'foo');
|
await fillIn('[data-test-ttl-value]', 'foo');
|
||||||
assert.equal(this.changeSpy.callCount, callCount, "it did't call onChange again");
|
assert.equal(this.changeSpy.callCount, callCount, "it didn't call onChange again");
|
||||||
assert.dom('[data-test-ttl-error]').includesText('Error', 'renders the error box');
|
assert.dom('[data-test-ttl-error]').includesText('Error', 'renders the error box');
|
||||||
|
|
||||||
await fillIn('[data-test-ttl-value]', '33');
|
await fillIn('[data-test-ttl-value]', '33');
|
||||||
assert.dom('[data-test-ttl-error]').doesNotIncludeText('Error', 'removes the error box');
|
assert.dom('[data-test-ttl-error]').doesNotExist('removes the error box');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it shows 30s for invalid duration initialValue input', async function (assert) {
|
test('it shows 30s for invalid duration initialValue input', async function (assert) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { collection, clickable, fillable, text, value } from 'ember-cli-page-object';
|
import { collection, clickable, fillable, text, value, isPresent } from 'ember-cli-page-object';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
tabs: collection('[data-test-auth-method]', {
|
tabs: collection('[data-test-auth-method]', {
|
||||||
|
@ -11,6 +11,7 @@ export default {
|
||||||
tokenValue: value('[data-test-token]'),
|
tokenValue: value('[data-test-token]'),
|
||||||
password: fillable('[data-test-password]'),
|
password: fillable('[data-test-password]'),
|
||||||
errorText: text('[data-test-auth-error]'),
|
errorText: text('[data-test-auth-error]'),
|
||||||
|
errorMessagePresent: isPresent('[data-test-auth-error]'),
|
||||||
descriptionText: text('[data-test-description]'),
|
descriptionText: text('[data-test-description]'),
|
||||||
login: clickable('[data-test-auth-submit]'),
|
login: clickable('[data-test-auth-submit]'),
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue