diff --git a/addons/api/addon/models/scope.js b/addons/api/addon/models/scope.js index 6bb0ea25c..c0db85cd1 100644 --- a/addons/api/addon/models/scope.js +++ b/addons/api/addon/models/scope.js @@ -4,13 +4,15 @@ */ import GeneratedScopeModel from '../generated/models/scope'; -import { computed } from '@ember/object'; -export const scopeTypes = { - global: 'global', - org: 'org', - project: 'project', -}; +export const TYPE_SCOPE_GLOBAL = 'global'; +export const TYPE_SCOPE_ORG = 'org'; +export const TYPE_SCOPE_PROJECT = 'project'; +export const TYPES_SCOPE = Object.freeze([ + TYPE_SCOPE_GLOBAL, + TYPE_SCOPE_ORG, + TYPE_SCOPE_PROJECT, +]); export default class ScopeModel extends GeneratedScopeModel { // =attributes @@ -18,9 +20,8 @@ export default class ScopeModel extends GeneratedScopeModel { /** * @type {boolean} */ - @computed('type') get isGlobal() { - return this.type === scopeTypes.global; + return this.type === TYPE_SCOPE_GLOBAL; } // There is only one global scope and it cannot be created by clients, // thus no set. @@ -28,23 +29,21 @@ export default class ScopeModel extends GeneratedScopeModel { /** * @type {boolean} */ - @computed('type') get isOrg() { - return this.type === scopeTypes.org; + return this.type === TYPE_SCOPE_ORG; } set isOrg(value) { - if (value) this.type = scopeTypes.org; + if (value) this.type = TYPE_SCOPE_ORG; } /** * @type {boolean} */ - @computed('type') get isProject() { - return this.type === scopeTypes.project; + return this.type === TYPE_SCOPE_PROJECT; } set isProject(value) { - if (value) this.type = scopeTypes.project; + if (value) this.type = TYPE_SCOPE_PROJECT; } /** diff --git a/addons/api/addon/services/indexed-db.js b/addons/api/addon/services/indexed-db.js index 93d207b8c..34b1ddd0a 100644 --- a/addons/api/addon/services/indexed-db.js +++ b/addons/api/addon/services/indexed-db.js @@ -16,6 +16,8 @@ export const modelIndexes = { group: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id', role: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id', + scope: + '&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id', 'credential-store': '&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id', 'host-catalog': diff --git a/ui/admin/app/components/targets/target/actions/index.hbs b/ui/admin/app/components/targets/target/actions/index.hbs index 5d93c8029..433b325cf 100644 --- a/ui/admin/app/components/targets/target/actions/index.hbs +++ b/ui/admin/app/components/targets/target/actions/index.hbs @@ -16,6 +16,7 @@ {{#if (can 'addBrokeredCredentialSources target' @model)}} {{t 'resources.target.actions.add-brokered-credential-sources'}} @@ -24,6 +25,7 @@ {{#if (can 'addInjectedApplicationCredentialSources target' @model)}} {{t 'resources.target.actions.add-injected-application-credential-sources' diff --git a/ui/admin/app/controllers/scopes/scope/scopes/index.js b/ui/admin/app/controllers/scopes/scope/scopes/index.js new file mode 100644 index 000000000..baec45fe8 --- /dev/null +++ b/ui/admin/app/controllers/scopes/scope/scopes/index.js @@ -0,0 +1,28 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { debounce } from 'core/decorators/debounce'; + +export default class ScopesScopeScopesIndexController extends Controller { + // =attributes + + queryParams = ['search', 'page', 'pageSize']; + + @tracked search = ''; + @tracked page = 1; + @tracked pageSize = 10; + + // =methods + + /** + * Handles input on each keystroke and the search queryParam + * @param {object} event + */ + @action + @debounce(250) + handleSearchInput(event) { + const { value } = event.target; + this.search = value; + this.page = 1; + } +} diff --git a/ui/admin/app/routes/onboarding.js b/ui/admin/app/routes/onboarding.js index 3a9e6232d..e7fb052ce 100644 --- a/ui/admin/app/routes/onboarding.js +++ b/ui/admin/app/routes/onboarding.js @@ -9,6 +9,7 @@ import { action } from '@ember/object'; import { loading } from 'ember-loading'; import { notifyError } from 'core/decorators/notify'; import { TYPE_TARGET_TCP } from 'api/models/target'; + export default class OnboardingRoute extends Route { // =services @@ -25,6 +26,7 @@ export default class OnboardingRoute extends Route { async beforeModel() { const orgs = await this.store.query('scope', { scope_id: 'global', + query: { filters: { scope_id: [{ equals: 'global' }] } }, }); if (!this.session.isAuthenticated || orgs.length) this.router.transitionTo('index'); diff --git a/ui/admin/app/routes/scopes.js b/ui/admin/app/routes/scopes.js index 6454ee690..e1111691c 100644 --- a/ui/admin/app/routes/scopes.js +++ b/ui/admin/app/routes/scopes.js @@ -33,9 +33,13 @@ export default class ScopesRoute extends Route { await this.store.findRecord('scope', 'global').catch(() => { /* no op */ }); - // NOTE: In the absence of a `scope_id` query parameter, this endpoint is - // expected to default to the global scope, thus returning org scopes. - return this.store.query('scope', {}).catch(() => A([])); + // Return all org scopes. + return this.store + .query('scope', { + scope_id: 'global', + query: { filters: { scope_id: [{ equals: 'global' }] } }, + }) + .catch(() => A([])); } /** diff --git a/ui/admin/app/routes/scopes/scope.js b/ui/admin/app/routes/scopes/scope.js index 79bcd97f3..6c1054f8f 100644 --- a/ui/admin/app/routes/scopes/scope.js +++ b/ui/admin/app/routes/scopes/scope.js @@ -11,6 +11,7 @@ import { next } from '@ember/runloop'; import { loading } from 'ember-loading'; import { confirm } from 'core/decorators/confirm'; import { notifySuccess, notifyError } from 'core/decorators/notify'; +import { TYPE_SCOPE_GLOBAL, TYPE_SCOPE_ORG } from 'api/models/scope'; export default class ScopesScopeRoute extends Route { // =services @@ -36,7 +37,7 @@ export default class ScopesScopeRoute extends Route { async model({ scope_id: id }) { // Since only global and org scopes are authenticatable, we can infer type // from ID because global has a fixed ID. - const type = id === 'global' ? 'global' : 'org'; + const type = id === 'global' ? TYPE_SCOPE_GLOBAL : TYPE_SCOPE_ORG; return this.store.findRecord('scope', id).catch(() => { const maybeExistingScope = this.store.peekRecord('scope', id); const scopeOptions = { id, type }; @@ -55,11 +56,17 @@ export default class ScopesScopeRoute extends Route { // First, load orgs and, if necessary, projects let orgs, projects; orgs = await this.store - .query('scope', { scope_id: 'global' }) + .query('scope', { + scope_id: 'global', + query: { filters: { scope_id: [{ equals: 'global' }] } }, + }) .catch(() => A([])); if (model.isProject) { - projects = await this.store.query('scope', { scope_id: model.scopeID }); + projects = await this.store.query('scope', { + scope_id: model.scopeID, + query: { filters: { scope_id: [{ equals: model.scopeID }] } }, + }); } // Then pull out the "selected" scopes, if relevant let selectedOrg, selectedProject; @@ -144,7 +151,6 @@ export default class ScopesScopeRoute extends Route { async delete(scope) { const { scopeID } = scope; await scope.destroyRecord(); - await this.router.replaceWith('scopes.scope.scopes', scopeID); - //this.refresh(); + this.router.replaceWith('scopes.scope.scopes', scopeID); } } diff --git a/ui/admin/app/routes/scopes/scope/roles/role/index.js b/ui/admin/app/routes/scopes/scope/roles/role/index.js index d6fdca467..152c832a3 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/index.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/index.js @@ -22,12 +22,16 @@ export default class ScopesScopeRolesRoleIndexRoute extends Route { if (!currentScope.isProject) { const scopes = await this.store.query('scope', { scope_id: currentScope.id, + query: { filters: { scope_id: [{ equals: currentScope.id }] } }, }); subScopes = await Promise.all( scopes.map(async (scope) => ({ model: scope, subScopes: !scope.isProject - ? await this.store.query('scope', { scope_id: scope.id }) + ? await this.store.query('scope', { + scope_id: scope.id, + query: { filters: { scope_id: [{ equals: scope.id }] } }, + }) : [], })), ); diff --git a/ui/admin/app/routes/scopes/scope/scopes.js b/ui/admin/app/routes/scopes/scope/scopes.js index 7e39d884b..d590b44a8 100644 --- a/ui/admin/app/routes/scopes/scope/scopes.js +++ b/ui/admin/app/routes/scopes/scope/scopes.js @@ -14,7 +14,6 @@ import { inject as service } from '@ember/service'; export default class ScopesScopeScopesRoute extends Route { // =services - @service store; @service session; @service router; @@ -26,23 +25,4 @@ export default class ScopesScopeScopesRoute extends Route { beforeModel() { if (!this.session.isAuthenticated) this.router.transitionTo('index'); } - - /** - * Loads sub scopes for the current scope. - * @return {Promise} - */ - async model() { - const currentScope = this.modelFor('scopes.scope'); - const parentScope = !currentScope.isGlobal - ? await this.store.findRecord('scope', currentScope.scopeID) - : null; - const subScopes = await this.store.query('scope', { - scope_id: currentScope.id, - }); - return { - currentScope, - parentScope, - subScopes, - }; - } } diff --git a/ui/admin/app/routes/scopes/scope/scopes/index.js b/ui/admin/app/routes/scopes/scope/scopes/index.js index 68550bf52..31000e5db 100644 --- a/ui/admin/app/routes/scopes/scope/scopes/index.js +++ b/ui/admin/app/routes/scopes/scope/scopes/index.js @@ -4,5 +4,84 @@ */ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; -export default class ScopesScopeScopesIndexRoute extends Route {} +export default class ScopesScopeScopesIndexRoute extends Route { + // =attributes + + queryParams = { + search: { + refreshModel: true, + replace: true, + }, + page: { + refreshModel: true, + }, + pageSize: { + refreshModel: true, + }, + }; + + // =services + + @service can; + @service store; + + // =methods + + /** + * Loads sub scopes for the current scope. + * @returns {Promise<{totalItems: number, currentScope: ScopeModel, subScopes: [ScopeModel], scopesExist: boolean }> } + */ + async model({ search, page, pageSize }) { + const currentScope = this.modelFor('scopes.scope'); + const { id: scope_id } = currentScope; + const filters = { + scope_id: [{ equals: scope_id }], + }; + + const subScopes = await this.store.query('scope', { + scope_id, + query: { search, filters }, + page, + pageSize, + }); + const totalItems = subScopes.meta?.totalItems; + const scopesExist = await this.getScopesExist(scope_id, totalItems); + + return { + currentScope, + subScopes, + scopesExist, + totalItems, + }; + } + + /** + * Sets scopesExist to true if there exists any scopes. + * @param {string} scope_id + * @param {number} totalItems + * @returns {Promise} + */ + async getScopesExist(scope_id, totalItems) { + if (totalItems > 0) { + return true; + } + const options = { pushToStore: false }; + const scopes = await this.store.query( + 'scope', + { + scope_id, + query: { + filters: { + scope_id: [{ equals: scope_id }], + }, + }, + page: 1, + pageSize: 1, + }, + options, + ); + return scopes.length > 0; + } +} diff --git a/ui/admin/app/routes/scopes/scope/storage-buckets/new.js b/ui/admin/app/routes/scopes/scope/storage-buckets/new.js index fce816c0a..be11fca22 100644 --- a/ui/admin/app/routes/scopes/scope/storage-buckets/new.js +++ b/ui/admin/app/routes/scopes/scope/storage-buckets/new.js @@ -78,7 +78,10 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route { async afterModel() { let scopes; const orgScopes = ( - await this.store.query('scope', { scope_id: 'global' }) + await this.store.query('scope', { + scope_id: 'global', + query: { filters: { scope_id: [{ equals: 'global' }] } }, + }) ).map((scope) => ({ model: scope })); scopes = [ { model: this.store.peekRecord('scope', 'global') }, diff --git a/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket/index.js b/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket/index.js index e2a7c1f89..c5329bf3e 100644 --- a/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket/index.js +++ b/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket/index.js @@ -16,7 +16,10 @@ export default class ScopesScopeStorageBucketsStorageBucketIndexRoute extends Ro async afterModel() { let scopes; const orgScopes = ( - await this.store.query('scope', { scope_id: 'global' }) + await this.store.query('scope', { + scope_id: 'global', + query: { filters: { scope_id: [{ equals: 'global' }] } }, + }) ).map((scope) => ({ model: scope })); scopes = [ { model: this.store.peekRecord('scope', 'global') }, diff --git a/ui/admin/app/routes/scopes/scope/targets/target/enable-session-recording/create-storage-bucket.js b/ui/admin/app/routes/scopes/scope/targets/target/enable-session-recording/create-storage-bucket.js index 720fbd608..358fb125b 100644 --- a/ui/admin/app/routes/scopes/scope/targets/target/enable-session-recording/create-storage-bucket.js +++ b/ui/admin/app/routes/scopes/scope/targets/target/enable-session-recording/create-storage-bucket.js @@ -5,13 +5,13 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; +import { loading } from 'ember-loading'; import { TYPE_STORAGE_BUCKET_PLUGIN, TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3, } from 'api/models/storage-bucket'; -import { action } from '@ember/object'; -import { notifySuccess, notifyError } from 'core/decorators/notify'; -import { loading } from 'ember-loading'; export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorageBucketRoute extends Route { // =services @@ -37,7 +37,10 @@ export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorage async afterModel() { let scopes; const orgScopes = ( - await this.store.query('scope', { scope_id: 'global' }) + await this.store.query('scope', { + scope_id: 'global', + query: { filters: { scope_id: [{ equals: 'global' }] } }, + }) ).map((scope) => ({ model: scope })); scopes = [ { model: this.store.peekRecord('scope', 'global') }, diff --git a/ui/admin/app/templates/scopes/scope/scopes/index.hbs b/ui/admin/app/templates/scopes/scope/scopes/index.hbs index 157c84323..d1b6b3d71 100644 --- a/ui/admin/app/templates/scopes/scope/scopes/index.hbs +++ b/ui/admin/app/templates/scopes/scope/scopes/index.hbs @@ -15,7 +15,7 @@ {{t 'resources.org.title_plural'}} - {{#if @model.subScopes}} + {{#if @model.scopesExist}}

{{t 'resources.org.description'}}

{{/if}} {{else if @model.currentScope.isOrg}} @@ -23,7 +23,7 @@ {{t 'resources.project.title_plural'}} - {{#if @model.subScopes}} + {{#if @model.scopesExist}}

{{t 'resources.project.description'}}

{{/if}} {{/if}} @@ -31,79 +31,115 @@ {{#if (can 'create model' @model.currentScope collection='scopes')}} - {{#if @model.subScopes}} - - {{#if @model.currentScope.isGlobal}} - {{t 'resources.org.titles.new'}} - {{else if @model.currentScope.isOrg}} - {{t 'resources.project.titles.new'}} - {{/if}} - + {{#if @model.scopesExist}} + {{/if}} {{/if}} - + - {{#if @model.subScopes}} - - - {{#each @model.subScopes as |scope|}} - + + + {{#if @model.subScopes}} + + <:body as |B|> + + + {{#if (can 'read model' B.data)}} + + {{B.data.displayName}} + + {{else}} + + {{B.data.displayName}} + + {{/if}} + + {{B.data.description}} + + + + + + + + + + {{else}} + + + - {{/each}} - + + {{/if}} {{else}} - - {{#if @model.currentScope.isGlobal}} - - - - {{t 'resources.org.description'}} - - {{#if (can 'create model' @model.currentScope collection='scopes')}} - - - {{t 'titles.new'}} - - {{/if}} - - - {{/if}} - - {{#if @model.currentScope.isOrg}} - - - - {{t 'resources.project.description'}} - - {{#if (can 'create model' @model.currentScope collection='scopes')}} - - - {{t 'titles.new'}} - - {{/if}} - - - {{/if}} - + + + + {{#if (can 'create model' @model.currentScope collection='scopes')}} + + + + {{/if}} + {{/if}} diff --git a/ui/admin/tests/acceptance/accounts/change-password-test.js b/ui/admin/tests/acceptance/accounts/change-password-test.js index ee929664b..e1ca94dd3 100644 --- a/ui/admin/tests/acceptance/accounts/change-password-test.js +++ b/ui/admin/tests/acceptance/accounts/change-password-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { Response } from 'miragejs'; import { @@ -19,6 +20,7 @@ import { module('Acceptance | accounts | change password', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { diff --git a/ui/admin/tests/acceptance/auth-methods/create-test.js b/ui/admin/tests/acceptance/auth-methods/create-test.js index 8ff402617..981d4351d 100644 --- a/ui/admin/tests/acceptance/auth-methods/create-test.js +++ b/ui/admin/tests/acceptance/auth-methods/create-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn, select } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { authenticateSession, @@ -18,6 +19,8 @@ import { module('Acceptance | auth-methods | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); + const DROPDOWN_SELECTOR_ICON = 'tbody .hds-table__tr:nth-child(1) .hds-table__td:last-child .hds-dropdown-toggle-icon'; const DROPDOWN_SELECTOR_OPTION = diff --git a/ui/admin/tests/acceptance/auth-methods/list-test.js b/ui/admin/tests/acceptance/auth-methods/list-test.js index fb7a71f5d..e5bf26d8b 100644 --- a/ui/admin/tests/acceptance/auth-methods/list-test.js +++ b/ui/admin/tests/acceptance/auth-methods/list-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, find } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession, // These are left here intentionally for future reference. @@ -17,6 +18,7 @@ import { module('Acceptance | auth-methods | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { diff --git a/ui/admin/tests/acceptance/auth-methods/read-test.js b/ui/admin/tests/acceptance/auth-methods/read-test.js index 562813c09..36a33fd62 100644 --- a/ui/admin/tests/acceptance/auth-methods/read-test.js +++ b/ui/admin/tests/acceptance/auth-methods/read-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession } from 'ember-simple-auth/test-support'; import { @@ -18,6 +19,8 @@ import { module('Acceptance | auth-methods | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); + const AUTH_LINK_SELECTOR = 'main tbody .hds-table__td:nth-child(1) a'; const AUTH_TYPE_SELECTOR = 'tbody .hds-table__tr:nth-child(1) .hds-table__td:nth-child(2)'; diff --git a/ui/admin/tests/acceptance/authentication-test.js b/ui/admin/tests/acceptance/authentication-test.js index abcd86446..eefc4e935 100644 --- a/ui/admin/tests/acceptance/authentication-test.js +++ b/ui/admin/tests/acceptance/authentication-test.js @@ -15,6 +15,7 @@ import { } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { runAllJobs } from 'ember-pollster/test-support'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; @@ -32,6 +33,7 @@ import { module('Acceptance | authentication', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let indexURL; let globalScope; diff --git a/ui/admin/tests/acceptance/policy/delete-test.js b/ui/admin/tests/acceptance/policy/delete-test.js index 7e1f14b23..358f4367c 100644 --- a/ui/admin/tests/acceptance/policy/delete-test.js +++ b/ui/admin/tests/acceptance/policy/delete-test.js @@ -7,11 +7,13 @@ import { module, test } from 'qunit'; import { visit, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | policies | delete', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; let policyCount; diff --git a/ui/admin/tests/acceptance/policy/list-test.js b/ui/admin/tests/acceptance/policy/list-test.js index 6977a41b8..4b55284f8 100644 --- a/ui/admin/tests/acceptance/policy/list-test.js +++ b/ui/admin/tests/acceptance/policy/list-test.js @@ -7,11 +7,13 @@ import { module, test } from 'qunit'; import { visit, click, currentURL, find } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | policies | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; let intl; diff --git a/ui/admin/tests/acceptance/policy/read-test.js b/ui/admin/tests/acceptance/policy/read-test.js index b888496c9..f0962fc92 100644 --- a/ui/admin/tests/acceptance/policy/read-test.js +++ b/ui/admin/tests/acceptance/policy/read-test.js @@ -7,12 +7,14 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | policies | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let features; diff --git a/ui/admin/tests/acceptance/scopes-test.js b/ui/admin/tests/acceptance/scopes-test.js index fa1395bb3..3fce9934f 100644 --- a/ui/admin/tests/acceptance/scopes-test.js +++ b/ui/admin/tests/acceptance/scopes-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { Response } from 'miragejs'; import { @@ -19,6 +20,15 @@ import { module('Acceptance | scopes', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); + + const ORG_SCOPES_DROPDOWN_SELECTOR = (url) => + `.rose-header-nav .rose-dropdown [href="${url}"]`; + const PROJECT_SCOPES_DROPDOWN_SELECTOR = (url) => + `.rose-header-nav .rose-dropdown + .rose-dropdown [href="${url}"]`; + const SAVE_BUTTON_SELECTOR = '[type="submit"]'; + const CANCEL_BUTTON_SELECTOR = '.rose-form-actions [type="button"]'; + const NAME_FIELD_SELECTOR = '[name="name"]'; let getScopeCount; @@ -34,10 +44,14 @@ module('Acceptance | scopes', function (hooks) { const urls = { globalScope: null, orgScope: null, + orgScopes: null, + newOrgScope: null, org2Scope: null, + org2Scopes: null, orgScopeEdit: null, org2ScopeEdit: null, projectScope: null, + newProjectScope: null, project2Scope: null, projectScopeEdit: null, project2ScopeEdit: null, @@ -64,19 +78,21 @@ module('Acceptance | scopes', function (hooks) { }); // Generate route URLs for resources urls.globalScope = `/scopes/global/scopes`; - urls.newOrgScope = `/scopes/global/scopes/new`; - urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; - urls.org2Scope = `/scopes/${instances.scopes.org2.id}/scopes`; - urls.orgScopeEdit = `/scopes/${instances.scopes.org.id}/edit`; - urls.org2ScopeEdit = `/scopes/${instances.scopes.org2.id}/edit`; - urls.newProjectScope = `/scopes/${instances.scopes.org.id}/scopes/new`; + urls.newOrgScope = `${urls.globalScope}/new`; + urls.orgScope = `/scopes/${instances.scopes.org.id}`; + urls.orgScopes = `${urls.orgScope}/scopes`; + urls.org2Scope = `/scopes/${instances.scopes.org2.id}`; + urls.org2Scopes = `${urls.org2Scope}/scopes`; + urls.orgScopeEdit = `${urls.orgScope}/edit`; + urls.org2ScopeEdit = `${urls.org2Scope}/edit`; + urls.newProjectScope = `${urls.orgScopes}/new`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.project2Scope = `/scopes/${instances.scopes.project2.id}`; - urls.projectScopeEdit = `/scopes/${instances.scopes.project.id}/edit`; - urls.project2ScopeEdit = `/scopes/${instances.scopes.project2.id}/edit`; - // Generate resource couner + urls.projectScopeEdit = `${urls.projectScope}/edit`; + urls.project2ScopeEdit = `${urls.project2Scope}/edit`; + // Generate resource counter getScopeCount = (type) => this.server.schema.scopes.where({ type }).length; - authenticateSession({}); + authenticateSession({ isGlobal: true }); }); test('visiting global scope', async function (assert) { @@ -100,67 +116,79 @@ module('Acceptance | scopes', function (hooks) { }); test('visiting org scope', async function (assert) { - await visit(urls.orgScope); + await visit(urls.orgScopes); await a11yAudit(); - assert.strictEqual(currentURL(), urls.orgScope); + + assert.strictEqual(currentURL(), urls.orgScopes); }); - // NOTE: In reality, we'd have a third "Global" item listed in the org - // dropdown. But since the mock test authenticator bypasses the auth - // normalization step, the UI doesn't know it's authenticated with global - // and thus doesn't display the "Global" item in the org nav dropdown. test('can navigate among org scopes via header navigation', async function (assert) { await visit(urls.globalScope); await a11yAudit(); + assert.strictEqual(currentURL(), urls.globalScope); - await click('.rose-header-nav .rose-dropdown a:nth-child(1)'); - assert.strictEqual(currentURL(), urls.orgScope); - // In reality, there would be a third item in the list - await click('.rose-header-nav .rose-dropdown a:nth-child(2)'); - assert.strictEqual(currentURL(), urls.org2Scope); + + await click(ORG_SCOPES_DROPDOWN_SELECTOR(urls.orgScope)); + assert.strictEqual(currentURL(), urls.orgScopes); + + await click(ORG_SCOPES_DROPDOWN_SELECTOR(urls.org2Scope)); + assert.strictEqual(currentURL(), urls.org2Scopes); }); test('can navigate among project scopes via header navigation', async function (assert) { await visit(urls.projectScope); await a11yAudit(); + assert.strictEqual(currentURL(), urls.projectScopeEdit); - await click( - '.rose-header-nav .rose-dropdown + .rose-dropdown a:nth-child(2)', - ); + + await click(PROJECT_SCOPES_DROPDOWN_SELECTOR(urls.project2Scope)); + assert.strictEqual(currentURL(), urls.project2ScopeEdit); }); test('can create new org scopes', async function (assert) { const orgScopeCount = getScopeCount('org'); - await visit(urls.newOrgScope); - await fillIn('[name="name"]', 'random string'); - await click('[type="submit"]'); + await visit(urls.globalScope); + + await click(`[href="${urls.newOrgScope}"]`); + await fillIn(NAME_FIELD_SELECTOR, 'random string'); + await click(SAVE_BUTTON_SELECTOR); + assert.strictEqual(getScopeCount('org'), orgScopeCount + 1); }); test('can create new project scopes', async function (assert) { const orgScopeCount = getScopeCount('project'); - await visit(urls.newProjectScope); - await fillIn('[name="name"]', 'random string'); - await click('[type="submit"]'); + await visit(urls.orgScopes); + + await click(`[href="${urls.newProjectScope}"]`); + await fillIn(NAME_FIELD_SELECTOR, 'random string'); + await click(SAVE_BUTTON_SELECTOR); + assert.strictEqual(getScopeCount('project'), orgScopeCount + 1); }); test('can cancel create new org scopes', async function (assert) { const orgScopeCount = getScopeCount('org'); - await visit(urls.newOrgScope); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="button"]'); + await visit(urls.globalScope); + + await click(`[href="${urls.newOrgScope}"]`); + await fillIn(NAME_FIELD_SELECTOR, 'random string'); + await click(CANCEL_BUTTON_SELECTOR); + assert.strictEqual(currentURL(), urls.globalScope); assert.strictEqual(getScopeCount('org'), orgScopeCount); }); test('can cancel create new project scopes', async function (assert) { const projectScopeCount = getScopeCount('project'); - await visit(urls.newProjectScope); - await fillIn('[name="name"]', 'random string'); - await click('.rose-form-actions [type="button"]'); - assert.strictEqual(currentURL(), urls.orgScope); + await visit(urls.orgScopes); + + await click(`[href="${urls.newProjectScope}"]`); + await fillIn(NAME_FIELD_SELECTOR, 'random string'); + await click(CANCEL_BUTTON_SELECTOR); + + assert.strictEqual(currentURL(), urls.orgScopes); assert.strictEqual(getScopeCount('project'), projectScopeCount); }); @@ -184,8 +212,11 @@ module('Acceptance | scopes', function (hooks) { }, ); }); - await visit(urls.newOrgScope); - await click('[type="submit"]'); + await visit(urls.globalScope); + + await click(`[href="${urls.newOrgScope}"]`); + await click(SAVE_BUTTON_SELECTOR); + assert.dom('.rose-notification-body').hasText('The request was invalid.'); assert.dom('.hds-form-error__message').hasText('Name is required.'); }); diff --git a/ui/admin/tests/acceptance/scopes/delete-test.js b/ui/admin/tests/acceptance/scopes/delete-test.js index aa45843e1..e1ab0d6df 100644 --- a/ui/admin/tests/acceptance/scopes/delete-test.js +++ b/ui/admin/tests/acceptance/scopes/delete-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, click, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | scopes | delete', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let getScopeCount; diff --git a/ui/admin/tests/acceptance/scopes/list-test.js b/ui/admin/tests/acceptance/scopes/list-test.js new file mode 100644 index 000000000..940925630 --- /dev/null +++ b/ui/admin/tests/acceptance/scopes/list-test.js @@ -0,0 +1,120 @@ +import { module, test } from 'qunit'; +import { visit, fillIn, waitFor } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; +import { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | scopes | list', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + setupIndexedDb(hooks); + + const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]'; + const NO_RESULTS_MSG_SELECTOR = '[data-test-no-scope-results]'; + const SCOPE_LINK_SELECTOR = (id) => + `tbody [data-test-scopes-table-row="${id}"] a`; + + const instances = { + scopes: { + global: null, + org1: null, + org2: null, + project1: null, + project2: null, + }, + }; + + const urls = { + globalScope: null, + orgScope: null, + }; + + hooks.beforeEach(function () { + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org1 = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.scopes.org2 = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.scopes.project1 = this.server.create('scope', { + type: 'project', + scope: { id: instances.scopes.org1.id, type: 'org' }, + }); + instances.scopes.project2 = this.server.create('scope', { + type: 'project', + scope: { id: instances.scopes.org1.id, type: 'org' }, + }); + urls.globalScope = `/scopes/global/scopes`; + urls.orgScope = `/scopes/${instances.scopes.org1.id}/scopes`; + authenticateSession({}); + }); + + test('user can search for a specifc org scope by id', async function (assert) { + await visit(urls.globalScope); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).exists(); + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org1.id); + await waitFor(SCOPE_LINK_SELECTOR(instances.scopes.org2.id), { count: 0 }); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).exists(); + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).doesNotExist(); + }); + + test('user can search for org scopes and get no results', async function (assert) { + await visit(urls.globalScope); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).exists(); + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, 'fake org scope that does not exist'); + await waitFor(NO_RESULTS_MSG_SELECTOR, { count: 1 }); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org1.id)).doesNotExist(); + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.org2.id)).doesNotExist(); + }); + + test('user can search for a specifc project scope by id', async function (assert) { + await visit(urls.orgScope); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)).exists(); + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id)).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.project1.id); + await waitFor(SCOPE_LINK_SELECTOR(instances.scopes.project2.id), { + count: 0, + }); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)).exists(); + assert + .dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id)) + .doesNotExist(); + }); + + test('user can search for project scopes and get no results', async function (assert) { + await visit(urls.orgScope); + + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)).exists(); + assert.dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id)).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, 'fake org scope that does not exist'); + await waitFor(NO_RESULTS_MSG_SELECTOR, { count: 1 }); + + assert + .dom(SCOPE_LINK_SELECTOR(instances.scopes.project1.id)) + .doesNotExist(); + assert + .dom(SCOPE_LINK_SELECTOR(instances.scopes.project2.id)) + .doesNotExist(); + }); +}); diff --git a/ui/admin/tests/acceptance/scopes/read-test.js b/ui/admin/tests/acceptance/scopes/read-test.js index 9342e9b1c..5b68abdc7 100644 --- a/ui/admin/tests/acceptance/scopes/read-test.js +++ b/ui/admin/tests/acceptance/scopes/read-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | scopes | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let features; let featureEdition; diff --git a/ui/admin/tests/acceptance/scopes/update-test.js b/ui/admin/tests/acceptance/scopes/update-test.js index 21c4bb9a1..d6c7ce046 100644 --- a/ui/admin/tests/acceptance/scopes/update-test.js +++ b/ui/admin/tests/acceptance/scopes/update-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | scopes | update', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { diff --git a/ui/admin/tests/acceptance/session-recordings/list-test.js b/ui/admin/tests/acceptance/session-recordings/list-test.js index 488072ae7..212308a29 100644 --- a/ui/admin/tests/acceptance/session-recordings/list-test.js +++ b/ui/admin/tests/acceptance/session-recordings/list-test.js @@ -7,11 +7,13 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | session recordings | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; diff --git a/ui/admin/tests/acceptance/storage-buckets/list-test.js b/ui/admin/tests/acceptance/storage-buckets/list-test.js index 94ed7563d..26ebce816 100644 --- a/ui/admin/tests/acceptance/storage-buckets/list-test.js +++ b/ui/admin/tests/acceptance/storage-buckets/list-test.js @@ -7,11 +7,13 @@ import { module, test } from 'qunit'; import { visit, click, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | storage-buckets | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; let intl; diff --git a/ui/admin/tests/acceptance/storage-buckets/read-test.js b/ui/admin/tests/acceptance/storage-buckets/read-test.js index fdaa18ae5..9e1a48871 100644 --- a/ui/admin/tests/acceptance/storage-buckets/read-test.js +++ b/ui/admin/tests/acceptance/storage-buckets/read-test.js @@ -7,12 +7,14 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession } from 'ember-simple-auth/test-support'; module('Acceptance | storage-buckets | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let features; diff --git a/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js b/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js index 6f8c260e2..650729e4d 100644 --- a/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js +++ b/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js @@ -246,7 +246,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) { (item) => item !== 'add-credential-sources', ); await visit(urls.brokeredCredentialSources); - assert.dom('.rose-dropdown-content a:nth-child(2)').doesNotExist(); + assert.dom('[data-test-add-brokered-cred-sources-action]').doesNotExist(); }); test('can select and cancel credential sources to add', async function (assert) { diff --git a/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js b/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js index 4587eba54..25cfbbd36 100644 --- a/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js +++ b/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js @@ -261,7 +261,7 @@ module( ); await visit(urls.injectedApplicationCredentialSources); - assert.dom('.rose-dropdown-content a:nth-child(2)').doesNotExist(); + assert.dom('[data-test-add-injected-cred-sources-action]').doesNotExist(); }); test('can select and cancel credential sources to add', async function (assert) { diff --git a/ui/admin/tests/acceptance/users/create-test.js b/ui/admin/tests/acceptance/users/create-test.js index f8aee7c97..3190826fc 100644 --- a/ui/admin/tests/acceptance/users/create-test.js +++ b/ui/admin/tests/acceptance/users/create-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | users | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let getUsersCount; diff --git a/ui/admin/tests/acceptance/workers/list-test.js b/ui/admin/tests/acceptance/workers/list-test.js index 07e049e8c..dbdb1ed35 100644 --- a/ui/admin/tests/acceptance/workers/list-test.js +++ b/ui/admin/tests/acceptance/workers/list-test.js @@ -7,6 +7,7 @@ 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 { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession, // These are left here intentionally for future reference. @@ -17,6 +18,7 @@ import { module('Acceptance | workers | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; diff --git a/ui/admin/tests/acceptance/workers/read-test.js b/ui/admin/tests/acceptance/workers/read-test.js index c36c3e237..213e37934 100644 --- a/ui/admin/tests/acceptance/workers/read-test.js +++ b/ui/admin/tests/acceptance/workers/read-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | workers | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; diff --git a/ui/admin/tests/unit/controllers/scopes/scope/scopes/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/scopes/index-test.js new file mode 100644 index 000000000..32d3b124a --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/scopes/index-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { waitUntil } from '@ember/test-helpers'; + +module('Unit | Controller | scopes/scope/scopes/index', function (hooks) { + setupTest(hooks); + + let controller; + + hooks.beforeEach(function () { + controller = this.owner.lookup('controller:scopes/scope/scopes/index'); + }); + + test('it exists', function (assert) { + assert.ok(controller); + }); + + test('handleSearchInput action sets expected values correctly', async function (assert) { + const searchValue = 'test'; + controller.handleSearchInput({ target: { value: searchValue } }); + await waitUntil(() => controller.search === searchValue); + + assert.strictEqual(controller.page, 1); + assert.strictEqual(controller.search, searchValue); + }); +});