-
-
-
- {{this.nodeUtilization.reservedCPUPercent}}
-
+
+
+ {{this.nodeUtilization.totalCPU}}
+
+ MHz
+
+
+ of CPU
+
+
+
+
+
+
+ {{this.nodeUtilization.reservedCPUPercent}}
+
+
+
+
+
+ {{format-percentage this.nodeUtilization.reservedCPUPercent total=1}}
+
-
-
{{format-percentage this.nodeUtilization.reservedCPUPercent total=1}}
+
+
+ {{format-scheduled-hertz this.nodeUtilization.totalReservedCPU}}
+
+ /
+ {{format-scheduled-hertz this.nodeUtilization.totalCPU}}
+ reserved
-
- {{format-scheduled-hertz this.nodeUtilization.totalReservedCPU}} / {{format-scheduled-hertz this.nodeUtilization.totalCPU}} reserved
-
-
{{/let}}
{{else if this.activeAllocation}}
- Allocation:
- {{this.activeAllocation.shortId}}
+
+ Allocation:
+
+
+ {{this.activeAllocation.shortId}}
+
-
Sibling Allocations: {{this.siblingAllocations.length}}
-
Unique Client Placements: {{this.uniqueActiveAllocationNodes.length}}
+
+
+ Sibling Allocations:
+
+ {{this.siblingAllocations.length}}
+
+
+
+ Unique Client Placements:
+
+ {{this.uniqueActiveAllocationNodes.length}}
+
- Job:
+
+ Job:
+
- {{this.activeAllocation.job.name}}
- / {{this.activeAllocation.taskGroupName}}
+ @query={{hash jobNamespace=this.activeAllocation.job.namespace.id}}
+ >
+ {{this.activeAllocation.job.name}}
+
+
+ /
+ {{this.activeAllocation.taskGroupName}}
+
-
Type: {{this.activeAllocation.job.type}}
-
Priority: {{this.activeAllocation.job.priority}}
+
+
+ Type:
+
+ {{this.activeAllocation.job.type}}
+
+
+
+ Priority:
+
+ {{this.activeAllocation.job.priority}}
+
- Client:
-
+
+ Client:
+
+
{{this.activeAllocation.node.shortId}}
-
Name: {{this.activeAllocation.node.name}}
-
Address: {{this.activeAllocation.node.httpAddr}}
+
+
+ Name:
+
+ {{this.activeAllocation.node.name}}
+
+
+
+ Address:
+
+ {{this.activeAllocation.node.httpAddr}}
+
{{else}}
-
{{this.model.nodes.length}} Clients
+
+ {{this.model.nodes.length}}
+
+ Clients
+
+
-
{{this.scheduledAllocations.length}} Allocations
+
+ {{this.scheduledAllocations.length}}
+
+ Allocations
+
+
-
{{this.totalMemoryFormatted}} {{this.totalMemoryUnits}} of memory
+
+ {{this.totalMemoryFormatted}}
+
+ {{this.totalMemoryUnits}}
+
+
+ of memory
+
+
@@ -191,21 +399,37 @@
data-test-memory-progress-bar
class="progress is-danger is-small"
value="{{this.reservedMemoryPercent}}"
- max="1">
+ max="1"
+ >
{{this.reservedMemoryPercent}}
- {{format-percentage this.reservedMemoryPercent total=1}}
+
+ {{format-percentage this.reservedMemoryPercent total=1}}
+
- {{format-bytes this.totalReservedMemory}} / {{format-bytes this.totalMemory}} reserved
+
+ {{format-bytes this.totalReservedMemory}}
+
+ /
+ {{format-bytes this.totalMemory}}
+ reserved
-
{{this.totalCPUFormatted}} {{this.totalCPUUnits}} of CPU
+
+ {{this.totalCPUFormatted}}
+
+ {{this.totalCPUUnits}}
+
+
+ of CPU
+
+
@@ -213,17 +437,25 @@
data-test-cpu-progress-bar
class="progress is-info is-small"
value="{{this.reservedCPUPercent}}"
- max="1">
+ max="1"
+ >
{{this.reservedCPUPercent}}
- {{format-percentage this.reservedCPUPercent total=1}}
+
+ {{format-percentage this.reservedCPUPercent total=1}}
+
- {{format-hertz this.totalReservedCPU}} / {{format-hertz this.totalCPU}} reserved
+
+ {{format-hertz this.totalReservedCPU}}
+
+ /
+ {{format-hertz this.totalCPU}}
+ reserved
{{/if}}
@@ -236,9 +468,10 @@
@allocations={{this.model.allocations}}
@onAllocationSelect={{action this.setAllocation}}
@onNodeSelect={{action this.setNode}}
- @onDataError={{action this.handleTopoVizDataError}}/>
+ @onDataError={{action this.handleTopoVizDataError}}
+ />
{{/if}}
-
+
\ No newline at end of file
diff --git a/ui/app/utils/breadcrumb-utils.js b/ui/app/utils/breadcrumb-utils.js
deleted file mode 100644
index 699d3f87522c..000000000000
--- a/ui/app/utils/breadcrumb-utils.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import PromiseObject from 'nomad-ui/utils/classes/promise-object';
-import { qpBuilder } from 'nomad-ui/utils/classes/query-params';
-
-export const jobCrumb = job => ({
- label: job.get('trimmedName'),
- args: [
- 'jobs.job.index',
- job.get('plainId'),
- qpBuilder({
- jobNamespace: job.get('namespace.name') || 'default',
- }),
- ],
-});
-
-export const jobCrumbs = job => {
- if (!job) return [];
-
- if (job.get('parent.content')) {
- return [
- PromiseObject.create({
- promise: job.get('parent').then(parent => jobCrumb(parent)),
- }),
- jobCrumb(job),
- ];
- } else {
- return [jobCrumb(job)];
- }
-};
diff --git a/ui/package.json b/ui/package.json
index 1777c0ebd8cb..1f7250733008 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -94,6 +94,7 @@
"ember-power-select": "^4.1.3",
"ember-qunit": "^4.6.0",
"ember-qunit-nice-errors": "^1.2.0",
+ "ember-render-helpers": "^0.2.0",
"ember-resolver": "^8.0.0",
"ember-responsive": "^3.0.4",
"ember-sinon": "^4.0.0",
diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js
index cfd9042adcb2..2b2049b5465a 100644
--- a/ui/tests/acceptance/client-detail-test.js
+++ b/ui/tests/acceptance/client-detail-test.js
@@ -68,8 +68,8 @@ module('Acceptance | client detail', function(hooks) {
);
assert.equal(
Layout.breadcrumbFor('clients.client').text,
- node.id.split('-')[0],
- 'Second breadcrumb says the node short id'
+ `Client ${node.id.split('-')[0]}`,
+ 'Second breadcrumb is a titled breadcrumb saying the node short id'
);
await Layout.breadcrumbFor('clients.index').visit();
assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
diff --git a/ui/tests/acceptance/client-monitor-test.js b/ui/tests/acceptance/client-monitor-test.js
index 49523009ce8b..c795345e600d 100644
--- a/ui/tests/acceptance/client-monitor-test.js
+++ b/ui/tests/acceptance/client-monitor-test.js
@@ -36,7 +36,7 @@ module('Acceptance | client monitor', function(hooks) {
await ClientMonitor.visit({ id: node.id });
assert.equal(Layout.breadcrumbFor('clients.index').text, 'Clients');
- assert.equal(Layout.breadcrumbFor('clients.client').text, node.id.split('-')[0]);
+ assert.equal(Layout.breadcrumbFor('clients.client').text, `Client ${node.id.split('-')[0]}`);
await Layout.breadcrumbFor('clients.index').visit();
assert.equal(currentURL(), '/clients');
diff --git a/ui/tests/acceptance/server-monitor-test.js b/ui/tests/acceptance/server-monitor-test.js
index 41cf3fc9afe1..0f6aeed3e02d 100644
--- a/ui/tests/acceptance/server-monitor-test.js
+++ b/ui/tests/acceptance/server-monitor-test.js
@@ -33,9 +33,12 @@ module('Acceptance | server monitor', function(hooks) {
test('/servers/:id/monitor should have a breadcrumb trail linking back to servers', async function(assert) {
await ServerMonitor.visit({ name: agent.name });
-
- assert.equal(Layout.breadcrumbFor('servers.index').text, 'Servers');
- assert.equal(Layout.breadcrumbFor('servers.server').text, agent.name);
+ assert.equal(
+ Layout.breadcrumbFor('servers.index').text,
+ 'Servers',
+ 'The page should read the breadcrumb Servers'
+ );
+ assert.equal(Layout.breadcrumbFor('servers.server').text, `Server ${agent.name}`);
await Layout.breadcrumbFor('servers.index').visit();
assert.equal(currentURL(), '/servers');
diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js
index e15f47b62747..becc9c212f1f 100644
--- a/ui/tests/acceptance/task-detail-test.js
+++ b/ui/tests/acceptance/task-detail-test.js
@@ -1,4 +1,4 @@
-import { currentURL } from '@ember/test-helpers';
+import { currentURL, waitFor } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
@@ -57,26 +57,27 @@ module('Acceptance | task detail', function(hooks) {
const job = server.db.jobs.find(jobId);
const shortId = allocation.id.split('-')[0];
-
assert.equal(Layout.breadcrumbFor('jobs.index').text, 'Jobs', 'Jobs is the first breadcrumb');
+
+ await waitFor('[data-test-job-breadcrumb]');
assert.equal(
Layout.breadcrumbFor('jobs.job.index').text,
- job.name,
+ `Job ${job.name}`,
'Job is the second breadcrumb'
);
assert.equal(
Layout.breadcrumbFor('jobs.job.task-group').text,
- taskGroup,
+ `Task Group ${taskGroup}`,
'Task Group is the third breadcrumb'
);
assert.equal(
Layout.breadcrumbFor('allocations.allocation').text,
- shortId,
+ `Allocation ${shortId}`,
'Allocation short id is the fourth breadcrumb'
);
assert.equal(
Layout.breadcrumbFor('allocations.allocation.task').text,
- task.name,
+ `Task ${task.name}`,
'Task name is the fifth breadcrumb'
);
diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js
index 795a227ee231..9f14c3f47617 100644
--- a/ui/tests/acceptance/task-group-detail-test.js
+++ b/ui/tests/acceptance/task-group-detail-test.js
@@ -122,12 +122,12 @@ module('Acceptance | task group detail', function(hooks) {
assert.equal(Layout.breadcrumbFor('jobs.index').text, 'Jobs', 'First breadcrumb says jobs');
assert.equal(
Layout.breadcrumbFor('jobs.job.index').text,
- job.name,
+ `Job ${job.name}`,
'Second breadcrumb says the job name'
);
assert.equal(
Layout.breadcrumbFor('jobs.job.task-group').text,
- taskGroup.name,
+ `Task Group ${taskGroup.name}`,
'Third breadcrumb says the job name'
);
});
diff --git a/ui/tests/integration/components/app-breadcrumbs-test.js b/ui/tests/integration/components/app-breadcrumbs-test.js
index 60ca4b1a0538..bc3d18c054db 100644
--- a/ui/tests/integration/components/app-breadcrumbs-test.js
+++ b/ui/tests/integration/components/app-breadcrumbs-test.js
@@ -1,49 +1,31 @@
-import Service from '@ember/service';
-import RSVP from 'rsvp';
+/* eslint-disable ember-a11y-testing/a11y-audit-called */
+import { setComponentTemplate } from '@ember/component';
+import Component from '@glimmer/component';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
-import { findAll, render, settled } from '@ember/test-helpers';
+import { findAll, render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
-import PromiseObject from 'nomad-ui/utils/classes/promise-object';
-import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
module('Integration | Component | app breadcrumbs', function(hooks) {
setupRenderingTest(hooks);
- hooks.beforeEach(function() {
- const mockBreadcrumbs = Service.extend({
- init() {
- this._super(...arguments);
- this.breadcrumbs = [];
- },
- });
-
- this.owner.register('service:breadcrumbs', mockBreadcrumbs);
- this.breadcrumbs = this.owner.lookup('service:breadcrumbs');
- });
-
- const commonCrumbs = [{ label: 'One', args: ['one'] }, { label: 'Two', args: ['two'] }];
-
- const template = hbs`
-
- `;
-
- test('breadcrumbs comes from the breadcrumbs service', async function(assert) {
- this.breadcrumbs.set('breadcrumbs', commonCrumbs);
-
- await render(template);
-
- assert.equal(
- findAll('[data-test-breadcrumb]').length,
- commonCrumbs.length,
- 'The number of crumbs matches the crumbs from the service'
- );
- });
+ const commonCrumbs = [
+ { label: 'Jobs', args: ['jobs.index'] },
+ { label: 'Job', args: ['jobs.job.index'] },
+ ];
test('every breadcrumb is rendered correctly', async function(assert) {
- this.breadcrumbs.set('breadcrumbs', commonCrumbs);
-
- await render(template);
+ this.set('commonCrumbs', commonCrumbs);
+ await render(hbs`
+
+ {{#each this.commonCrumbs as |crumb|}}
+
+ {{/each}}
+ `);
+
+ assert
+ .dom('[data-test-breadcrumb-default]')
+ .exists('We register the default breadcrumb component if no type is specified on the crumb');
const renderedCrumbs = findAll('[data-test-breadcrumb]');
@@ -56,36 +38,39 @@ module('Integration | Component | app breadcrumbs', function(hooks) {
});
});
- test('when breadcrumbs are pending promises, an ellipsis is rendered', async function(assert) {
- let resolvePromise;
- const promise = new RSVP.Promise(resolve => {
- resolvePromise = resolve;
- });
-
- this.breadcrumbs.set('breadcrumbs', [
- { label: 'One', args: ['one'] },
- PromiseObject.create({ promise }),
- { label: 'Three', args: ['three'] },
- ]);
-
- await render(template);
-
- assert.equal(
- findAll('[data-test-breadcrumb]')[1].textContent.trim(),
- '…',
- 'Promise breadcrumb is in a loading state'
+ test('when we register a crumb with a type property, a dedicated breadcrumb/
component renders', async function(assert) {
+ const crumbs = [
+ { label: 'Jobs', args: ['jobs.index'] },
+ { type: 'special', label: 'Job', args: ['jobs.job.index'] },
+ ];
+ this.set('crumbs', crumbs);
+
+ class MockComponent extends Component {}
+ this.owner.register(
+ 'component:breadcrumbs/special',
+ setComponentTemplate(
+ hbs`
+ Test
+ `,
+ MockComponent
+ )
);
- await componentA11yAudit(this.element, assert);
-
- resolvePromise({ label: 'Two', args: ['two'] });
-
- return settled().then(() => {
- assert.equal(
- findAll('[data-test-breadcrumb]')[1].textContent.trim(),
- 'Two',
- 'Promise breadcrumb has resolved and now renders Two'
+ await render(hbs`
+
+ {{#each this.crumbs as |crumb|}}
+
+ {{/each}}
+ `);
+
+ assert
+ .dom('[data-test-breadcrumb-special]')
+ .exists(
+ 'We can create a new type of breadcrumb component and AppBreadcrumbs will handle rendering by type'
);
- });
+
+ assert
+ .dom('[data-test-breadcrumb-default]')
+ .exists('Default breadcrumb registers if no type is specified');
});
});
diff --git a/ui/tests/integration/components/breadcrumbs-test.js b/ui/tests/integration/components/breadcrumbs-test.js
new file mode 100644
index 000000000000..5d24329ff0bf
--- /dev/null
+++ b/ui/tests/integration/components/breadcrumbs-test.js
@@ -0,0 +1,67 @@
+/* eslint-disable ember-a11y-testing/a11y-audit-called */
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { click, findAll, render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | breadcrumbs', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it declaratively renders a list of registered crumbs', async function(assert) {
+ this.set('isRegistered', false);
+ this.set('toggleCrumb', () => this.set('isRegistered', !this.isRegistered));
+ await render(hbs`
+
+
+ {{#each bb as |crumb|}}
+ {{crumb.args.crumb}}
+ {{/each}}
+
+
+ Toggle
+
+ {{#if this.isRegistered}}
+
+ {{/if}}
+ `);
+
+ assert.dom('[data-test-crumb]').exists({ count: 1 }, 'We register one crumb');
+ assert.dom('[data-test-crumb]').hasText('Zoey', 'The first registered crumb is Zoey');
+
+ await click('[data-test-button]');
+ const crumbs = await findAll('[data-test-crumb]');
+
+ assert
+ .dom('[data-test-crumb]')
+ .exists({ count: 2 }, 'The second crumb registered successfully');
+ assert
+ .dom(crumbs[0])
+ .hasText('Zoey', 'Breadcrumbs maintain the order in which they are declared');
+ assert
+ .dom(crumbs[1])
+ .hasText('Tomster', 'Breadcrumbs maintain the order in which they are declared');
+
+ await click('[data-test-button]');
+ assert.dom('[data-test-crumb]').exists({ count: 1 }, 'We deregister one crumb');
+ assert
+ .dom('[data-test-crumb]')
+ .hasText('Zoey', 'Zoey remains in the template after Tomster deregisters');
+ });
+
+ test('it can register complex crumb objects', async function(assert) {
+ await render(hbs`
+
+
+ {{#each bb as |crumb|}}
+ {{crumb.args.crumb.name}}
+ {{/each}}
+
+
+
+ `);
+
+ assert
+ .dom('[data-test-crumb]')
+ .hasText('Tomster', 'We can access the registered breadcrumbs in the template');
+ });
+});
diff --git a/ui/tests/integration/components/trigger-test.js b/ui/tests/integration/components/trigger-test.js
new file mode 100644
index 000000000000..bbcb41b32488
--- /dev/null
+++ b/ui/tests/integration/components/trigger-test.js
@@ -0,0 +1,191 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, click, waitFor } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | trigger', function(hooks) {
+ setupRenderingTest(hooks);
+
+ module('Synchronous Interactions', function() {
+ test('it can trigger a synchronous action', async function(assert) {
+ this.set('name', 'Tomster');
+ this.set('changeName', () => this.set('name', 'Zoey'));
+ await render(hbs`
+
+ {{this.name}}
+ Change my name.
+
+ `);
+ assert.dom('[data-test-name]').hasText('Tomster', 'Initial state renders correctly.');
+
+ await click('[data-test-button]');
+
+ assert
+ .dom('[data-test-name]')
+ .hasText('Zoey', 'The name property changes when the button is clicked');
+ });
+
+ test('it sets the result of the action', async function(assert) {
+ this.set('tomster', () => 'Tomster');
+ await render(hbs`
+
+ {{#if trigger.data.result}}
+ {{trigger.data.result}}
+ {{/if}}
+ Generate
+
+ `);
+ assert
+ .dom('[data-test-name]')
+ .doesNotExist('Initial state does not render because there is no result yet.');
+
+ await click('[data-test-button]');
+
+ assert
+ .dom('[data-test-name]')
+ .hasText('Tomster', 'The result state updates after the triggered action');
+ });
+ });
+
+ module('Asynchronous Interactions', function() {
+ test('it can trigger an asynchronous action', async function(assert) {
+ this.set(
+ 'onTrigger',
+ () =>
+ new Promise(resolve => {
+ this.set('resolve', resolve);
+ })
+ );
+
+ await render(hbs`
+
+ {{#if trigger.data.isBusy}}
+ ...Loading
+ {{/if}}
+ {{#if trigger.data.isSuccess}}
+ Success!
+ {{/if}}
+ Click Me
+
+ `);
+
+ assert
+ .dom('[data-test-div]')
+ .doesNotExist('The div does not render until after the action dispatches successfully');
+
+ await click('[data-test-button]');
+ assert
+ .dom('[data-test-div-loading]')
+ .exists('Loading state is displayed when the action hasnt resolved yet');
+ assert
+ .dom('[data-test-div]')
+ .doesNotExist('Success message does not display until after promise resolves');
+
+ this.resolve();
+ await waitFor('[data-test-div]');
+ assert
+ .dom('[data-test-div-loading]')
+ .doesNotExist(
+ 'Loading state is no longer rendered after state changes from busy to success'
+ );
+ assert
+ .dom('[data-test-div]')
+ .exists('Action has dispatched successfully after the promise resolves');
+
+ await click('[data-test-button]');
+ assert
+ .dom('[data-test-div]')
+ .doesNotExist('Aftering clicking the button, again, the state is reset');
+ assert
+ .dom('[data-test-div-loading]')
+ .exists('After clicking the button, again, we are back in the loading state');
+
+ this.resolve();
+ await waitFor('[data-test-div]');
+
+ assert
+ .dom('[data-test-div]')
+ .exists(
+ 'An new action and new promise resolve after clicking the button for the second time'
+ );
+ });
+
+ test('it handles the success state', async function(assert) {
+ this.set(
+ 'onTrigger',
+ () =>
+ new Promise(resolve => {
+ this.set('resolve', resolve);
+ })
+ );
+ this.set('onSuccess', () => assert.step('On success happened'));
+
+ await render(hbs`
+
+ {{#if trigger.data.isSuccess}}
+ Success!
+ {{/if}}
+ Click Me
+
+ `);
+
+ assert
+ .dom('[data-test-div]')
+ .doesNotExist('No text should appear until after the onSuccess callback is fired');
+ await click('[data-test-button]');
+ this.resolve();
+ await waitFor('[data-test-div]');
+ assert.verifySteps(['On success happened']);
+ });
+
+ test('it handles the error state', async function(assert) {
+ this.set(
+ 'onTrigger',
+ () =>
+ new Promise((_, reject) => {
+ this.set('reject', reject);
+ })
+ );
+ this.set('onError', () => {
+ assert.step('On error happened');
+ });
+
+ await render(hbs`
+
+ {{#if trigger.data.isBusy}}
+ ...Loading
+ {{/if}}
+ {{#if trigger.data.isError}}
+ Error!
+ {{/if}}
+ Click Me
+
+ `);
+
+ await click('[data-test-button]');
+ assert
+ .dom('[data-test-div-loading]')
+ .exists('Loading state is displayed when the action hasnt resolved yet');
+
+ assert
+ .dom('[data-test-div]')
+ .doesNotExist('No text should appear until after the onError callback is fired');
+
+ this.reject();
+ await waitFor('[data-test-span]');
+ assert.verifySteps(['On error happened']);
+
+ await click('[data-test-button]');
+
+ assert
+ .dom('[data-test-div-loading]')
+ .exists('The previous error state was cleared and we show loading, again.');
+
+ assert.dom('[data-test-div]').doesNotExist('The error state is cleared');
+
+ this.reject();
+ await waitFor('[data-test-span]');
+ assert.verifySteps(['On error happened'], 'The error dispatches');
+ });
+ });
+});
diff --git a/ui/tests/unit/services/breadcrumbs-test.js b/ui/tests/unit/services/breadcrumbs-test.js
deleted file mode 100644
index e71c7eb3a673..000000000000
--- a/ui/tests/unit/services/breadcrumbs-test.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import Service from '@ember/service';
-import Route from '@ember/routing/route';
-import Controller from '@ember/controller';
-import { get } from '@ember/object';
-import { alias } from '@ember/object/computed';
-import RSVP from 'rsvp';
-import { module, test } from 'qunit';
-import { setupTest } from 'ember-qunit';
-import PromiseObject from 'nomad-ui/utils/classes/promise-object';
-
-const makeRoute = (crumbs, controller = {}) =>
- Route.extend({
- breadcrumbs: crumbs,
- controller: Controller.extend(controller).create(),
- });
-
-module('Unit | Service | Breadcrumbs', function(hooks) {
- setupTest(hooks);
-
- hooks.beforeEach(function() {
- this.subject = function() {
- return this.owner.factoryFor('service:breadcrumbs').create();
- };
- });
-
- hooks.beforeEach(function() {
- const mockRouter = Service.extend({
- currentRouteName: 'application',
- currentURL: '/',
- });
-
- this.owner.register('service:router', mockRouter);
- this.router = this.owner.lookup('service:router');
-
- const none = makeRoute();
- const fixed = makeRoute([{ label: 'Static', args: ['static.index'] }]);
- const manyFixed = makeRoute([
- { label: 'Static 1', args: ['static.index', 1] },
- { label: 'Static 2', args: ['static.index', 2] },
- ]);
- const dynamic = makeRoute(model => [{ label: model, args: ['dynamic.index', model] }], {
- model: 'Label of the Crumb',
- });
- const manyDynamic = makeRoute(
- model => [
- { label: get(model, 'fishOne'), args: ['dynamic.index', get(model, 'fishOne')] },
- { label: get(model, 'fishTwo'), args: ['dynamic.index', get(model, 'fishTwo')] },
- ],
- {
- model: {
- fishOne: 'red',
- fishTwo: 'blue',
- },
- }
- );
- const promise = makeRoute([
- PromiseObject.create({
- promise: RSVP.Promise.resolve({
- label: 'delayed',
- args: ['wait.for.it'],
- }),
- }),
- ]);
- const fromURL = makeRoute(model => [{ label: model, args: ['url'] }], {
- router: this.owner.lookup('service:router'),
- model: alias('router.currentURL'),
- });
-
- this.owner.register('route:none', none);
- this.owner.register('route:none.more-none', none);
- this.owner.register('route:static', fixed);
- this.owner.register('route:static.many', manyFixed);
- this.owner.register('route:dynamic', dynamic);
- this.owner.register('route:dynamic.many', manyDynamic);
- this.owner.register('route:promise', promise);
- this.owner.register('route:url', fromURL);
- });
-
- test('when the route hierarchy has no breadcrumbs', function(assert) {
- this.router.set('currentRouteName', 'none');
-
- const service = this.subject();
- assert.deepEqual(service.get('breadcrumbs'), []);
- });
-
- test('when the route hierarchy has one segment with static crumbs', function(assert) {
- this.router.set('currentRouteName', 'static');
-
- const service = this.subject();
- assert.deepEqual(service.get('breadcrumbs'), [{ label: 'Static', args: ['static.index'] }]);
- });
-
- test('when the route hierarchy has multiple segments with static crumbs', function(assert) {
- this.router.set('currentRouteName', 'static.many');
-
- const service = this.subject();
- assert.deepEqual(service.get('breadcrumbs'), [
- { label: 'Static', args: ['static.index'] },
- { label: 'Static 1', args: ['static.index', 1] },
- { label: 'Static 2', args: ['static.index', 2] },
- ]);
- });
-
- test('when the route hierarchy has a function as its breadcrumbs property', function(assert) {
- this.router.set('currentRouteName', 'dynamic');
-
- const service = this.subject();
- assert.deepEqual(service.get('breadcrumbs'), [
- { label: 'Label of the Crumb', args: ['dynamic.index', 'Label of the Crumb'] },
- ]);
- });
-
- test('when the route hierarchy has multiple segments with dynamic crumbs', function(assert) {
- this.router.set('currentRouteName', 'dynamic.many');
-
- const service = this.subject();
- assert.deepEqual(service.get('breadcrumbs'), [
- { label: 'Label of the Crumb', args: ['dynamic.index', 'Label of the Crumb'] },
- { label: 'red', args: ['dynamic.index', 'red'] },
- { label: 'blue', args: ['dynamic.index', 'blue'] },
- ]);
- });
-
- test('when a route provides a breadcrumb that is a promise, it gets passed through to the template', function(assert) {
- this.router.set('currentRouteName', 'promise');
-
- const service = this.subject();
- assert.ok(service.get('breadcrumbs.firstObject') instanceof PromiseObject);
- });
-
- // This happens when transitioning to the current route but with a different model
- // jobs.job.index --> jobs.job.index
- // /jobs/one --> /jobs/two
- test('when the route stays the same but the url changes, breadcrumbs get recomputed', function(assert) {
- this.router.set('currentRouteName', 'url');
-
- const service = this.subject();
- assert.deepEqual(
- service.get('breadcrumbs'),
- [{ label: '/', args: ['url'] }],
- 'The label is initially / as is the router currentURL'
- );
-
- this.router.set('currentURL', '/somewhere/else');
- assert.deepEqual(
- service.get('breadcrumbs'),
- [{ label: '/somewhere/else', args: ['url'] }],
- 'The label changes with currentURL since it is an alias and a change to currentURL recomputes breadcrumbs'
- );
- });
-});
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 586b7893459b..fefe48c547bc 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -4345,6 +4345,13 @@ ansi-to-html@^0.6.11, ansi-to-html@^0.6.6:
dependencies:
entities "^1.1.2"
+ansi-to-html@^0.6.15:
+ version "0.6.15"
+ resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.15.tgz#ac6ad4798a00f6aa045535d7f6a9cb9294eebea7"
+ integrity sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==
+ dependencies:
+ entities "^2.0.0"
+
ansicolors@~0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef"
@@ -8925,6 +8932,22 @@ ember-cli-typescript@^3.0.0, ember-cli-typescript@^3.1.3, ember-cli-typescript@^
stagehand "^1.0.0"
walk-sync "^2.0.0"
+ember-cli-typescript@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-4.2.1.tgz#54d08fc90318cc986f3ea562f93ce58a6cc4c24d"
+ integrity sha512-0iKTZ+/wH6UB/VTWKvGuXlmwiE8HSIGcxHamwNhEC5x1mN3z8RfvsFZdQWYUzIWFN2Tek0gmepGRPTwWdBYl/A==
+ dependencies:
+ ansi-to-html "^0.6.15"
+ broccoli-stew "^3.0.0"
+ debug "^4.0.0"
+ execa "^4.0.0"
+ fs-extra "^9.0.1"
+ resolve "^1.5.0"
+ rsvp "^4.8.1"
+ semver "^7.3.2"
+ stagehand "^1.0.0"
+ walk-sync "^2.2.0"
+
ember-cli-typescript@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-4.1.0.tgz#2ff17be2e6d26b58c88b1764cb73887e7176618b"
@@ -9426,6 +9449,14 @@ ember-qunit@^4.6.0:
ember-cli-test-loader "^2.2.0"
qunit "^2.9.3"
+ember-render-helpers@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/ember-render-helpers/-/ember-render-helpers-0.2.0.tgz#5f7af8ee74ae29f85e0d156b2775edff23f6de21"
+ integrity sha512-MnqGS8BnY3GJ+n5RZVVRqCwKjfXXMr5quKyqNu1vxft8oslOJuZ1f1dOesQouD+6LwD4Y9tWRVKNw+LOqM9ocw==
+ dependencies:
+ ember-cli-babel "^7.23.0"
+ ember-cli-typescript "^4.0.0"
+
ember-resolver@^8.0.0:
version "8.0.2"
resolved "https://registry.yarnpkg.com/ember-resolver/-/ember-resolver-8.0.2.tgz#8a45a744aaf5391eb52b4cb393b3b06d2db1975c"