ICU 5247 detailed worker view and ICU 5248 edit worker view (#1246)
* feat: 🎸 Add list view for workers ✅ Closes: ICU-5251 * feat: 🎸 Add route for worker detail and include links in table * refactor: 💡 Updated id display in table and fixed worker route * test: 💍 Add workers list test * feat: 🎸 Add worker detailed view and edit view Added worker detailed view and editable form view for name and description of worker ✅ Closes: ICU-5247 ICU-5248 * refactor: 💡 fixed conflict merge fixed merge conflicts for scope/workers/worker ✅ Closes: ICU-5247 * Update ui/admin/app/routes/scopes/scope/workers.js Co-authored-by: Dharini Jeeva <dharini@hashicorp.com> * Update ui/admin/app/templates/scopes/scope/workers/worker/index.hbs Co-authored-by: Dharini Jeeva <dharini@hashicorp.com> * test: 💍 Test for worker/update-test.js Created update-test.js and refactored worker/form.hbs for correct display of last_seen ✅ Closes: ICU-5247 * Update ui/admin/app/routes/scopes/scope/workers.js Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> * Update ui/admin/app/templates/scopes/scope/workers/worker/index.hbs Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> * Update ui/admin/tests/acceptance/workers/read-test.js Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> * refactor: 💡 code refactor and test creation Refactored code for detailed worker view, created controller for detailew worker view to display worker name in breadcrumbs, added test for `delete`, `update`. ✅ Closes: ICU-5247 * refactor: 💡 Refactor test Refactor update-test.js and clean up code. ✅ Closes: ICU-5247 * refactor: 💡 fixed workers/update-test.js added local variable for better test coverage for cancel test in workers/update-test.js ✅ Closes: ICU-5247 * refactor: 💡 fix test Refactored yaml file for `last_seen.description` ✅ Closes: ICU-5247 * Update ui/admin/app/routes/scopes/scope/workers.js Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> * removed `type` from `<form.textarea/>` component * Update ui/admin/app/controllers/scopes/scope/workers/worker.js Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> * spacing for linter * refactor: 💡 Refactor test Refactored `worker/read-test.js` to to test correct authorization ✅ Closes: ICU-5247 * ci: 🎡 Fixed lint fixed lint ✅ Closes: ICU-5247 * refactor: 💡 Added custom relatvie date to field Added custom relative date to `last_status_time` input for worker view. ✅ Closes: ICU-5247 * refactor: 💡 refactored yaml file changed delete dropdown for worker to `actions.delete` ✅ Closes: ICU-5247 * Removed unused services from workers.js * merge changes from main * fix lint * fix lint * Update index.hbs lint issues * refactor: 💡 fixed lint for hbs fixed linting issues for hbs files ✅ Closes: ICU-5247 ICU-5249 Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> Co-authored-by: Dharini Jeeva <dharini@hashicorp.com>
This commit is contained in:
13 files changed
+505
-7
No files matched your search
@@ -4,7 +4,7 @@ import { attr } from '@ember-data/model';
|
||||
export default class WorkerModel extends GeneratedWorkerModel {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
/**
|
||||
* Names are optional on models in our API. Thus we need to fallback on ID
|
||||
* for display purposes.
|
||||
* @type {string}
|
||||
|
||||
@@ -10,8 +10,9 @@ export default Factory.extend({
|
||||
description: () => faker.random.words(),
|
||||
created_time: () => faker.date.recent(),
|
||||
updated_time: () => faker.date.recent(),
|
||||
version: () => faker.datatype.number(),
|
||||
address: () => `${faker.internet.ipv4()}:${faker.internet.port()}`,
|
||||
version: () => faker.datatype.number({ max: 4 }),
|
||||
address: () =>
|
||||
`${faker.datatype.uuid()}.proxy.boundary.hashicorp.cloud:${faker.internet.port()}`,
|
||||
last_status_time: () => faker.date.recent(),
|
||||
active_connection_count: () => faker.datatype.number({ max: 10 }),
|
||||
});
|
||||
@@ -41,6 +41,17 @@ session_max_seconds:
|
||||
session_connection_limit:
|
||||
label: Maximum Connections
|
||||
help: The maximum number of connections allowed per session. For unlimited, specify "-1".
|
||||
worker_version:
|
||||
label: Version
|
||||
worker_authorized_actions:
|
||||
label: Authorized Actions
|
||||
worker_last_seen:
|
||||
label: Last Seen
|
||||
help: Last time the worker reported status.
|
||||
worker_address:
|
||||
help: URL and port for this worker.
|
||||
worker_type:
|
||||
help: Type of worker
|
||||
worker_filter:
|
||||
label: Worker Filter
|
||||
help: Optional boolean expression to filter the workers that are allowed to satisfy this request.
|
||||
|
||||
@@ -404,7 +404,7 @@ credential:
|
||||
password:
|
||||
label: Password
|
||||
help: The password to use when signing in with this credential store.
|
||||
private_key:
|
||||
private_key:
|
||||
label: SSH Private Key
|
||||
help: The private key to use when connecting with this credential store.
|
||||
passphrase:
|
||||
@@ -438,6 +438,7 @@ worker:
|
||||
description: Workers are the server components that perform the session handling.
|
||||
actions:
|
||||
register: Register Worker
|
||||
delete: Remove Worker
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Workers
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<Rose::Form
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@showEditToggle={{if @model.isNew false true}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
{{#unless form.isEditable}}
|
||||
<form.input
|
||||
@name='type'
|
||||
@value={{@model.type}}
|
||||
@label={{t 'form.type.label'}}
|
||||
@helperText={{t 'form.worker_type.help'}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
<form.input
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
{{/if}}
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
|
||||
{{#unless form.isEditable}}
|
||||
<form.input
|
||||
@name='address'
|
||||
@type='url'
|
||||
@value={{@model.address}}
|
||||
@label={{t 'form.address.label'}}
|
||||
@helperText={{t 'form.worker_address.help'}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
{{#let
|
||||
(relative-datetime-live @model.last_status_time)
|
||||
(format-date @model.last_status_time format='yymmddt')
|
||||
as |relativeDate customFormat|
|
||||
}}
|
||||
<form.input
|
||||
@name='last seen'
|
||||
@type='datetime'
|
||||
@value={{concat relativeDate ', ' customFormat}}
|
||||
@label={{t 'form.worker_last_seen.label'}}
|
||||
@helperText={{t 'form.worker_last_seen.help'}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if (can 'save worker' @model)}}
|
||||
<form.actions
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</Rose::Form>
|
||||
@@ -0,0 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeWorkersWorkerController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* A worker breadcrumb
|
||||
* @type {string}
|
||||
*/
|
||||
get breadCrumb() {
|
||||
return this.model.displayName;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,14 @@ export default {
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
},
|
||||
yymmddt: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
},
|
||||
},
|
||||
number: {
|
||||
compact: { notation: 'compact' },
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { loading } from 'ember-loading';
|
||||
import { confirm } from 'core/decorators/confirm';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
|
||||
|
||||
export default class ScopesScopeWorkersRoute extends Route {
|
||||
// =services
|
||||
|
||||
|
||||
@service can
|
||||
@service session;
|
||||
@service router;
|
||||
|
||||
@@ -24,4 +29,40 @@ export default class ScopesScopeWorkersRoute extends Route {
|
||||
refresh() {
|
||||
return super.refresh(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a worker in current scope.
|
||||
* @param {WorkerModel} worker
|
||||
* @param {Event} e
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@notifyError(({ message }) => message)
|
||||
@notifySuccess(({ isNew }) =>
|
||||
isNew ? 'notifications.create-success' : 'notifications.save-success'
|
||||
)
|
||||
async save(worker) {
|
||||
await worker.save();
|
||||
if (this.can.can('read model', worker)) {
|
||||
await this.router.transitionTo('scopes.scope.workers.worker', worker);
|
||||
} else {
|
||||
await this.router.transitionTo('scopes.scope.workers');
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete worker in current scope and redirect to index.
|
||||
* @param {UserModel} worker
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('questions.delete-confirm')
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(worker) {
|
||||
await worker.destroyRecord();
|
||||
await this.router.replaceWith('scopes.scope.workers');
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,49 @@
|
||||
{{page-title 'Worker'}}
|
||||
{{outlet}}
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.breadcrumbs>
|
||||
<BreadCrumbs />
|
||||
</page.breadcrumbs>
|
||||
|
||||
<page.header>
|
||||
<h2>
|
||||
{{t 'resources.worker.title'}}
|
||||
<DocLink @doc='worker' @iconSize='large' />
|
||||
</h2>
|
||||
<p>{{t 'resources.worker.description'}}</p>
|
||||
<Copyable
|
||||
@text={{@model.id}}
|
||||
@buttonText={{t 'actions.copy-to-clipboard'}}
|
||||
@acknowledgeText={{t 'states.copied'}}
|
||||
>
|
||||
<code>{{@model.id}}</code>
|
||||
</Copyable>
|
||||
</page.header>
|
||||
|
||||
<page.actions>
|
||||
<Rose::Dropdown
|
||||
@text='{{t 'actions.manage'}}'
|
||||
@dropdownRight={{true}}
|
||||
as |dropdown|
|
||||
>
|
||||
{{#if (can 'delete worker' @model)}}
|
||||
<dropdown.button
|
||||
@style='danger'
|
||||
@disabled={{@model.canSave}}
|
||||
{{on 'click' (route-action 'delete' @model)}}
|
||||
>
|
||||
{{t 'resources.worker.actions.delete'}}
|
||||
</dropdown.button>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</page.actions>
|
||||
|
||||
<page.body>
|
||||
<Rose::Separator />
|
||||
<Form::Worker
|
||||
@model={{@model}}
|
||||
@submit={{route-action 'save' @model}}
|
||||
@cancel={{route-action 'cancel' @model}}
|
||||
/>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -0,0 +1,108 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, find, click } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
import { resolve, reject } from 'rsvp';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
authenticateSession,
|
||||
// These are left here intentionally for future reference.
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
|
||||
module('Acceptance | workers | delete', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
let getWorkerCount;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
},
|
||||
};
|
||||
|
||||
const urls = {
|
||||
globalScope: null,
|
||||
workers: null,
|
||||
worker: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
// Generate resources
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.worker = this.server.create('worker', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
|
||||
urls.globalScope = `/scopes/global`;
|
||||
urls.workers = `${urls.globalScope}/workers`;
|
||||
urls.worker = `${urls.workers}/${instances.worker.id}`;
|
||||
|
||||
getWorkerCount = () => this.server.schema.workers.all().models.length;
|
||||
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('can delete a worker', async function (assert) {
|
||||
assert.expect(1);
|
||||
const count = getWorkerCount();
|
||||
await visit(urls.worker);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.strictEqual(getWorkerCount(), count - 1);
|
||||
});
|
||||
|
||||
test('can accept delete worker via dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
confirmService.enabled = true;
|
||||
confirmService.confirm = sinon.fake.returns(resolve());
|
||||
const count = getWorkerCount();
|
||||
await visit(urls.worker);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.strictEqual(getWorkerCount(), count - 1);
|
||||
assert.ok(confirmService.confirm.calledOnce);
|
||||
});
|
||||
|
||||
test('can cancel delete worker via dialog', async function (assert) {
|
||||
assert.expect(2);
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
confirmService.enabled = true;
|
||||
confirmService.confirm = sinon.fake.returns(reject());
|
||||
const count = getWorkerCount();
|
||||
await visit(urls.worker);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.strictEqual(getWorkerCount(), count);
|
||||
assert.ok(confirmService.confirm.calledOnce);
|
||||
});
|
||||
|
||||
test('cannot delete worker without proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.worker.authorized_actions =
|
||||
instances.worker.authorized_actions.filter((item) => item !== 'delete');
|
||||
await visit(urls.worker);
|
||||
assert
|
||||
.dom('.rose-layout-page-actions .rose-dropdown-button-danger')
|
||||
.isNotVisible();
|
||||
});
|
||||
|
||||
test('deleting a worker which errors displays error messages', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.del('/workers/:id', () => {
|
||||
return new Response(
|
||||
490,
|
||||
{},
|
||||
{
|
||||
status: 490,
|
||||
code: 'error',
|
||||
message: 'Oops.',
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.worker);
|
||||
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
|
||||
assert.ok(find('[role="alert"]').textContent.trim(), 'Oops.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import {
|
||||
authenticateSession,
|
||||
// These are left here intentionally for future reference.
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
|
||||
module('Acceptance | workers | read', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
},
|
||||
worker: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
globalScope: null,
|
||||
workers: null,
|
||||
worker: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
//Generate the resources
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.worker = this.server.create('worker', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
urls.globalScope = '/scopes/global';
|
||||
urls.workers = `${urls.globalScope}/workers`;
|
||||
urls.worker = `${urls.workers}/${instances.worker.id}`;
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('visiting workers', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.workers);
|
||||
await a11yAudit();
|
||||
console.log(urls.workers);
|
||||
assert.dom(`[href="${urls.workers}"]`).isVisible();
|
||||
});
|
||||
|
||||
test('cannot navigate to an worker form without proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.worker.authorized_actions =
|
||||
instances.worker.authorized_actions.filter((itm) => itm !== 'read');
|
||||
await visit(urls.workers);
|
||||
await a11yAudit();
|
||||
assert
|
||||
.dom('main tbody .rose-table-header-cell:nth-child(1) a')
|
||||
.isNotVisible();
|
||||
});
|
||||
|
||||
test('can navigate to an worker form with proper authorization', async function (assert) {
|
||||
assert.expect(1);
|
||||
await visit(urls.workers);
|
||||
await a11yAudit();
|
||||
assert.dom('main tbody .rose-table-header-cell:nth-child(1) a').isVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, click, find, fillIn } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
import {
|
||||
authenticateSession,
|
||||
// These are left here intentionally for future reference.
|
||||
//currentSession,
|
||||
//invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
|
||||
module('Acceptance | workers | update', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
},
|
||||
};
|
||||
|
||||
const urls = {
|
||||
globalScope: null,
|
||||
workers: null,
|
||||
worker: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
//Generate the resources
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.worker = this.server.create('worker', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
// Generate route URLs for resources
|
||||
urls.globalScope = '/scopes/global';
|
||||
urls.workers = `${urls.globalScope}/workers`;
|
||||
(urls.worker = `${urls.workers}/${instances.worker.id}`),
|
||||
authenticateSession({});
|
||||
});
|
||||
|
||||
test('can save changes to an existing worker', async function (assert) {
|
||||
assert.expect(2);
|
||||
await visit(urls.worker);
|
||||
await click('form [type="button"]', 'Click edit mode');
|
||||
await fillIn('[name="name"]', 'Updated worker name');
|
||||
await click('.rose-form-actions [type="submit"]');
|
||||
assert.dom(`[href="${urls.worker}"]`).isVisible();
|
||||
await assert.dom('input[name="name"]').hasValue('Updated worker name');
|
||||
});
|
||||
|
||||
test('can cancel changes to an existing worker', async function (assert) {
|
||||
assert.expect(1);
|
||||
const name = instances.worker.name;
|
||||
await visit(urls.worker);
|
||||
await click('form [type="button"]', 'Click edit mode');
|
||||
await fillIn('[name="name"]', `Updated Worker Name`);
|
||||
await click('.rose-form-actions [type="button"]');
|
||||
await assert.dom('input[name="name"]').hasValue(`${name}`);
|
||||
});
|
||||
|
||||
test('saving an existing worker with invalid fields displays error messages', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.patch('/workers/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'invalid_argument',
|
||||
message: 'The request was invalid.',
|
||||
details: {
|
||||
request_fields: [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'Name is required',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
await visit(urls.worker);
|
||||
await click('.rose-form-actions [type="button"]', 'Click edit mode');
|
||||
await fillIn('[name="name"]', 'Worker Name');
|
||||
await click('[type="submit"]');
|
||||
assert.strictEqual(
|
||||
find('.rose-notification-body').textContent.trim(),
|
||||
'The request was invalid.',
|
||||
'Displays primary error message.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
find('.rose-form-error-message').textContent.trim(),
|
||||
'Name is required',
|
||||
'Displays field-level errors.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/users/user', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/workers/worker'
|
||||
);
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user