From a7053b4e8351ae4ede76a08a63b39d0b02f03560 Mon Sep 17 00:00:00 2001 From: Elliot Yibaebi Date: Thu, 26 Dec 2024 15:25:44 +0100 Subject: [PATCH] complete storeknox discovery pages testing --- .../ak-svg/storeknox-playstore-logo.hbs | 1 + .../ak-svg/storeknox-search-apps.hbs | 1 + app/components/storeknox/discover/index.hbs | 7 +- app/components/storeknox/discover/index.ts | 20 +- .../discover/requested-apps/index.hbs | 24 +- .../discover/requested-apps/index.ts | 2 +- .../discover/requested-apps/status/index.hbs | 27 +- .../discover/results/empty/index.hbs | 24 +- .../storeknox/discover/results/index.hbs | 55 +- .../storeknox/discover/results/index.ts | 21 +- .../results/table/action-header/index.ts | 6 +- .../discover/results/table/action/index.hbs | 25 +- .../discover/results/table/action/index.ts | 4 +- .../discover/results/table/index.hbs | 7 +- .../storeknox/discover/results/table/index.ts | 10 +- .../storeknox/table-columns/store/index.hbs | 6 +- app/models/sk-app-metadata.ts | 4 + app/models/sk-discovery-result.ts | 3 + mirage/factories/sk-app-metadata.ts | 34 + mirage/factories/sk-app.ts | 61 ++ mirage/factories/sk-discovery-result.ts | 32 + mirage/factories/sk-discovery.ts | 10 + mirage/factories/sk-requested-app.ts | 41 + mirage/models/sk-app-metadata.ts | 3 + mirage/models/sk-app.ts | 5 + mirage/models/sk-discovery-result.ts | 3 + mirage/models/sk-discovery.ts | 3 + mirage/models/sk-requested-app.ts | 5 + .../discovery/requested-apps-test.js | 199 +++++ .../storeknox/discovery/results-test.js | 777 ++++++++++++++++++ 30 files changed, 1347 insertions(+), 73 deletions(-) create mode 100644 mirage/factories/sk-app-metadata.ts create mode 100644 mirage/factories/sk-app.ts create mode 100644 mirage/factories/sk-discovery-result.ts create mode 100644 mirage/factories/sk-discovery.ts create mode 100644 mirage/factories/sk-requested-app.ts create mode 100644 mirage/models/sk-app-metadata.ts create mode 100644 mirage/models/sk-app.ts create mode 100644 mirage/models/sk-discovery-result.ts create mode 100644 mirage/models/sk-discovery.ts create mode 100644 mirage/models/sk-requested-app.ts create mode 100644 tests/acceptance/storeknox/discovery/requested-apps-test.js create mode 100644 tests/acceptance/storeknox/discovery/results-test.js diff --git a/app/components/ak-svg/storeknox-playstore-logo.hbs b/app/components/ak-svg/storeknox-playstore-logo.hbs index a4aeb556f..127dadca9 100644 --- a/app/components/ak-svg/storeknox-playstore-logo.hbs +++ b/app/components/ak-svg/storeknox-playstore-logo.hbs @@ -4,6 +4,7 @@ viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg' + ...attributes > - + {{t 'storeknox.discoverHeader'}} {{t 'storeknox.discoverDescription'}} @@ -23,6 +27,7 @@ @currentWhen={{item.activeRoutes}} @hasBadge={{item.hasBadge}} @badgeCount={{item.badgeCount}} + data-test-storeknox-discovery-tabs='{{item.id}}-tab' > {{item.label}} diff --git a/app/components/storeknox/discover/index.ts b/app/components/storeknox/discover/index.ts index 569170ebe..a36ecbb7b 100644 --- a/app/components/storeknox/discover/index.ts +++ b/app/components/storeknox/discover/index.ts @@ -17,7 +17,7 @@ export default class StoreknoxDiscoverComponent extends Component { constructor(owner: unknown, args: object) { super(owner, args); - if (!this.me.org?.is_admin) { + if (!this.me.org?.is_owner) { this.showWelcomeModal = false; } } @@ -29,19 +29,11 @@ export default class StoreknoxDiscoverComponent extends Component { route: 'authenticated.storeknox.discover.result', label: this.intl.t('storeknox.discoveryResults'), }, - this.me.org?.is_admin - ? { - id: 'pending-review', - route: 'authenticated.storeknox.discover.review', - label: this.intl.t('storeknox.pendingReview'), - hasBadge: true, - badgeCount: this.skPendingReview.totalCount, - } - : { - id: 'requested-apps', - route: 'authenticated.storeknox.discover.requested', - label: this.intl.t('storeknox.requestedApps'), - }, + !this.me.org?.is_owner && { + id: 'requested-apps', + route: 'authenticated.storeknox.discover.requested', + label: this.intl.t('storeknox.requestedApps'), + }, ].filter(Boolean); } diff --git a/app/components/storeknox/discover/requested-apps/index.hbs b/app/components/storeknox/discover/requested-apps/index.hbs index 733e85b5d..bf6fd88cc 100644 --- a/app/components/storeknox/discover/requested-apps/index.hbs +++ b/app/components/storeknox/discover/requested-apps/index.hbs @@ -3,17 +3,29 @@ @direction='column' @alignItems='center' local-class='empty-container' + data-test-storeknoxDiscover-requestedAppsTable-tableEmpty > - + - + {{t 'storeknox.noRequestedAppsFound'}} - + {{t 'storeknox.noRequestedAppsFoundDescription' htmlSafe=true}} + {{else}} - + {{#let (component r.columnValue.cellComponent) as |Component|}} <:icon> - + {{else}} - + {{this.statusDetails.text}} - + {{t 'by'}} {{this.statusDetails.by}} @@ -25,14 +35,21 @@ - + {{this.statusDetails.date}} <:default> - + diff --git a/app/components/storeknox/discover/results/empty/index.hbs b/app/components/storeknox/discover/results/empty/index.hbs index 3f47e6317..95cae21b6 100644 --- a/app/components/storeknox/discover/results/empty/index.hbs +++ b/app/components/storeknox/discover/results/empty/index.hbs @@ -1,12 +1,26 @@ - + + - - - + {{t 'storeknox.searchForApps'}} - + {{t 'storeknox.searchForAppsDescription'}} \ No newline at end of file diff --git a/app/components/storeknox/discover/results/index.hbs b/app/components/storeknox/discover/results/index.hbs index 724bc6d23..a773d83ec 100644 --- a/app/components/storeknox/discover/results/index.hbs +++ b/app/components/storeknox/discover/results/index.hbs @@ -4,28 +4,48 @@ <:rightAdornment> {{#if this.searchQuery}} - + {{else}} - + {{/if}} - + {{t 'storeknox.discoverHeader'}} - + - + @@ -42,6 +62,7 @@ @typographyFontWeight='bold' class='ml-1' {{on 'click' this.viewMore}} + data-test-storeknoxDiscover-results-viewMoreDisclaimerInfo > {{t 'viewMore'}} @@ -54,7 +75,6 @@ @goToPage={{this.goToPage}} @onItemPerPageChange={{this.onItemPerPageChange}} @skDiscoveryResultData={{this.skDiscoveryResultData}} - @selectedResults={{this.selectedResults}} /> {{else}} @@ -68,9 +88,15 @@ class='pr-1' as |ab| > - + - + - + - + {{t 'storeknox.disclaimerHeader'}} - + {{t 'storeknox.disclaimerBody' htmlSafe=true}} diff --git a/app/components/storeknox/discover/results/index.ts b/app/components/storeknox/discover/results/index.ts index 914ec4aa0..fcc3df062 100644 --- a/app/components/storeknox/discover/results/index.ts +++ b/app/components/storeknox/discover/results/index.ts @@ -1,19 +1,17 @@ -import Component from '@glimmer/component'; +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import type { DS } from 'ember-data'; +import { task } from 'ember-concurrency'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; import type RouterService from '@ember/routing/router-service'; import type Store from '@ember-data/store'; import type IntlService from 'ember-intl/services/intl'; -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import type { DS } from 'ember-data'; -import { task } from 'ember-concurrency'; +import parseError from 'irene/utils/parse-error'; import type { StoreknoxDiscoveryResultQueryParam } from 'irene/routes/authenticated/storeknox/discover/result'; import type SkDiscoverySearchResultModel from 'irene/models/sk-discovery-result'; -import parseError from 'irene/utils/parse-error'; -import type MeService from 'irene/services/me'; -import type SkPendingReviewService from 'irene/services/sk-pending-review'; export type SkDiscoveryResultResponse = DS.AdapterPopulatedRecordArray & { @@ -34,11 +32,10 @@ interface LimitOffset { export default class StoreknoxDiscoverResultsComponent extends Component { @service declare store: Store; @service declare router: RouterService; - @service('notifications') declare notify: NotificationService; @service declare intl: IntlService; + + @service('notifications') declare notify: NotificationService; @service('browser/window') declare window: Window; - @service declare me: MeService; - @service declare skPendingReview: SkPendingReviewService; @tracked searchQuery = ''; @tracked showDiscoveryResults = false; @@ -62,10 +59,6 @@ export default class StoreknoxDiscoverResultsComponent extends Component {{else}} {{#if this.requested}} - + <:tooltipContent>
@@ -24,6 +29,7 @@ @iconName={{this.iconValue.iconName}} @size='small' local-class='{{this.iconValue.className}}' + data-test-storeknoxDiscover-resultsTable-addedOrRequestedIcon /> @@ -31,11 +37,20 @@ {{#if this.buttonLoading}} {{else}} - - {{#if this.isAdmin}} - + + {{#if this.isOwner}} + {{else}} - + {{/if}} {{/if}} diff --git a/app/components/storeknox/discover/results/table/action/index.ts b/app/components/storeknox/discover/results/table/action/index.ts index 1bca8f83e..3b05e8f69 100644 --- a/app/components/storeknox/discover/results/table/action/index.ts +++ b/app/components/storeknox/discover/results/table/action/index.ts @@ -28,8 +28,8 @@ export default class StoreknoxDiscoverResultsTableActionComponent extends Compon @tracked approved: boolean = false; @tracked buttonLoading: boolean = false; - get isAdmin() { - return this.me.org?.is_admin; + get isOwner() { + return this.me.org?.is_owner; } get iconValue() { diff --git a/app/components/storeknox/discover/results/table/index.hbs b/app/components/storeknox/discover/results/table/index.hbs index 55f73df07..33eeb8922 100644 --- a/app/components/storeknox/discover/results/table/index.hbs +++ b/app/components/storeknox/discover/results/table/index.hbs @@ -13,6 +13,7 @@ @justifyContent='space-between' @alignItems='center' local-class='result-header' + data-test-storeknoxDiscover-resultsTable-header > @@ -80,7 +81,11 @@ - + {{#let (component r.columnValue.cellComponent) as |Component|}} void; onItemPerPageChange: (args: LimitOffset) => void; }; @@ -99,16 +99,16 @@ export default class StoreknoxDiscoverResultsTableComponent extends Component {{else}} {{#if @data.isIos}} - + {{/if}} {{#if @data.isAndroid}} - + {{/if}} {{/if}} \ No newline at end of file diff --git a/app/models/sk-app-metadata.ts b/app/models/sk-app-metadata.ts index 453d4ff92..8852e98c7 100644 --- a/app/models/sk-app-metadata.ts +++ b/app/models/sk-app-metadata.ts @@ -1,4 +1,8 @@ import Model, { attr } from '@ember-data/model'; +import Inflector from 'ember-inflector'; + +const inflector = Inflector.inflector; +inflector.irregular('sk-app-metadata', 'sk-app-metadata'); export interface Region { id: number; diff --git a/app/models/sk-discovery-result.ts b/app/models/sk-discovery-result.ts index 66942c652..b7ee3a9d0 100644 --- a/app/models/sk-discovery-result.ts +++ b/app/models/sk-discovery-result.ts @@ -39,6 +39,9 @@ export default class SkDiscoverySearchResultModel extends Model { @attr('string') declare description: string; + @attr('string') + declare devEmail: string; + @attr('string') declare devName: string; diff --git a/mirage/factories/sk-app-metadata.ts b/mirage/factories/sk-app-metadata.ts new file mode 100644 index 000000000..854985a23 --- /dev/null +++ b/mirage/factories/sk-app-metadata.ts @@ -0,0 +1,34 @@ +import { faker } from '@faker-js/faker'; +import { Factory } from 'miragejs'; +import ENUMS from 'irene/enums'; + +export default Factory.extend({ + doc_ulid: () => faker.string.uuid(), + doc_hash: () => faker.string.hexadecimal({ length: 64 }), + app_id: () => faker.string.uuid(), + url: () => faker.internet.url(), + icon_url: () => faker.image.url(), + package_name: () => faker.internet.domainWord(), + title: () => faker.company.name(), + platform: () => faker.number.int({ min: 0, max: 4 }), + dev_name: () => faker.name.fullName(), + dev_email: () => faker.internet.email(), + dev_website: () => faker.internet.url(), + dev_id: () => faker.string.uuid(), + rating: () => faker.number.float({ min: 0, max: 5, precision: 0.1 }), + rating_count: () => faker.number.int({ min: 1, max: 10000 }), + review_count: () => faker.number.int({ min: 1, max: 5000 }), + total_downloads: () => faker.number.int({ min: 1, max: 1000000 }), + upload_date: () => faker.date.past({ years: 1 }), + latest_upload_date: () => faker.date.recent(), + + region: () => ({ + id: faker.number.int({ min: 1, max: 100 }), + sk_store: faker.number.int({ min: 1, max: 100 }), + country_code: faker.location.countryCode(), + icon: faker.image.url(), + }), + + platform_display: () => + faker.helpers.arrayElement(ENUMS.PLATFORM.BASE_CHOICES.map((c) => c.key)), +}); diff --git a/mirage/factories/sk-app.ts b/mirage/factories/sk-app.ts new file mode 100644 index 000000000..b82e8f43f --- /dev/null +++ b/mirage/factories/sk-app.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-expect-error "trait" prop missing from miragejs +import { Factory, ModelInstance, Server, trait } from 'miragejs'; +import { faker } from '@faker-js/faker'; + +import ENUMS from 'irene/enums'; + +export default Factory.extend({ + approval_status: () => + faker.helpers.arrayElement(ENUMS.SK_APPROVAL_STATUS.VALUES), + + approval_status_display() { + const approval_status = this.approval_status as number; + + return ENUMS.SK_APPROVAL_STATUS?.BASE_CHOICES.find( + (c) => c.value === approval_status + )?.key; + }, + + app_status: () => faker.helpers.arrayElement(ENUMS.SK_APP_STATUS.VALUES), + + app_status_display() { + const app_status = this.app_status as number; + + return ENUMS.SK_APP_STATUS.BASE_CHOICES.find((c) => c.value === app_status) + ?.key; + }, + + monitoring_enabled: () => faker.datatype.boolean(), + monitoring_status: () => faker.number.int({ min: 0, max: 3 }), + + approved_on: () => faker.date.past(), + added_on: () => faker.date.past(), + updated_on: () => faker.date.recent(), + rejected_on: () => faker.date.past(), + + availability: () => ({ + storeknox: faker.datatype.boolean(), + appknox: faker.datatype.boolean(), + }), + + // @ts-expect-error + afterCreate(skApp: ModelInstance, server: Server) { + // @ts-expect-error + if (!skApp.app_metadata) { + skApp.update({ + app_metadata: server.create('sk-app-metadata'), + }); + } + }, + + withPendingReviewStatus: trait({ + approval_status: ENUMS.SK_APPROVAL_STATUS.PENDING_APPROVAL, + app_status: ENUMS.SK_APP_STATUS.ACTIVE, + }), + + withApprovedStatus: trait({ + approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, + app_status: ENUMS.SK_APP_STATUS.ACTIVE, + }), +}); diff --git a/mirage/factories/sk-discovery-result.ts b/mirage/factories/sk-discovery-result.ts new file mode 100644 index 000000000..e0987ff49 --- /dev/null +++ b/mirage/factories/sk-discovery-result.ts @@ -0,0 +1,32 @@ +import { Factory } from 'miragejs'; +import { faker } from '@faker-js/faker'; + +export default Factory.extend({ + doc_ulid: () => faker.string.uuid(), + doc_hash: () => faker.string.hexadecimal({ length: 64 }), + app_id: () => faker.string.uuid(), + package_name: () => faker.lorem.word(), + title: () => faker.commerce.productName(), + store: () => faker.helpers.arrayElement(['playstore', 'appstore']), + platform: () => faker.number.int({ min: 0, max: 1 }), + region: () => faker.location.countryCode(), + app_size: () => faker.number.int(), + app_type: () => faker.helpers.arrayElement(['application', 'game']), + app_url: () => faker.internet.url(), + is_free: () => faker.datatype.boolean(), + description: () => faker.lorem.paragraphs(2), + dev_name: () => faker.company.name(), + dev_email: () => faker.internet.email(), + icon_url: () => faker.image.url(), + latest_upload_date: () => faker.date.past(), + rating: () => faker.number.int({ min: 0, max: 5 }), + rating_count: () => faker.number.int(), + screenshots: () => Array.from({ length: 8 }, () => faker.image.imageUrl()), + version: () => faker.string.numeric(), + doc_created_on: () => faker.date.recent(), + doc_updated_on: () => faker.date.recent(), + doc_updated_on_ts: () => faker.number.int(), + + min_os_required: () => + faker.helpers.arrayElement(['5.0 and up', '9.0 and up']), +}); diff --git a/mirage/factories/sk-discovery.ts b/mirage/factories/sk-discovery.ts new file mode 100644 index 000000000..df42b7487 --- /dev/null +++ b/mirage/factories/sk-discovery.ts @@ -0,0 +1,10 @@ +import { Factory } from 'miragejs'; +import { faker } from '@faker-js/faker'; + +export default Factory.extend({ + query: () => ({ + q: faker.word.sample(), + }), + + continuous_discovery: () => faker.datatype.boolean(), +}); diff --git a/mirage/factories/sk-requested-app.ts b/mirage/factories/sk-requested-app.ts new file mode 100644 index 000000000..04d1eb1aa --- /dev/null +++ b/mirage/factories/sk-requested-app.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-expect-error "trait" prop missing from miragejs +import { ModelInstance, Server, trait } from 'miragejs'; +import { faker } from '@faker-js/faker'; + +import ENUMS from 'irene/enums'; +import SkAppFactory from './sk-app'; + +export default SkAppFactory.extend({ + withPendingApproval: trait({ + approved_by: null, + approved_on: null, + rejected_by: null, + rejected_on: null, + approval_status: ENUMS.SK_APPROVAL_STATUS.PENDING_APPROVAL, + }), + + withApproval: trait({ + approved_by: () => faker.person.firstName(), + rejected_by: null, + rejected_on: null, + approval_status: ENUMS.SK_APPROVAL_STATUS.APPROVED, + }), + + withRejection: trait({ + approved_by: null, + approved_on: null, + rejected_by: () => faker.person.firstName(), + approval_status: ENUMS.SK_APPROVAL_STATUS.REJECTED, + }), + + // @ts-expect-error + afterCreate(skApp: ModelInstance, server: Server) { + // @ts-expect-error + if (!skApp.app_metadata) { + skApp.update({ + app_metadata: server.create('sk-app-metadata'), + }); + } + }, +}); diff --git a/mirage/models/sk-app-metadata.ts b/mirage/models/sk-app-metadata.ts new file mode 100644 index 000000000..db502f142 --- /dev/null +++ b/mirage/models/sk-app-metadata.ts @@ -0,0 +1,3 @@ +import { Model } from 'miragejs'; + +export default Model.extend({}); diff --git a/mirage/models/sk-app.ts b/mirage/models/sk-app.ts new file mode 100644 index 000000000..c67cfc619 --- /dev/null +++ b/mirage/models/sk-app.ts @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'miragejs'; + +export default Model.extend({ + app_metadata: belongsTo('sk-app-metadata'), +}); diff --git a/mirage/models/sk-discovery-result.ts b/mirage/models/sk-discovery-result.ts new file mode 100644 index 000000000..db502f142 --- /dev/null +++ b/mirage/models/sk-discovery-result.ts @@ -0,0 +1,3 @@ +import { Model } from 'miragejs'; + +export default Model.extend({}); diff --git a/mirage/models/sk-discovery.ts b/mirage/models/sk-discovery.ts new file mode 100644 index 000000000..db502f142 --- /dev/null +++ b/mirage/models/sk-discovery.ts @@ -0,0 +1,3 @@ +import { Model } from 'miragejs'; + +export default Model.extend({}); diff --git a/mirage/models/sk-requested-app.ts b/mirage/models/sk-requested-app.ts new file mode 100644 index 000000000..c67cfc619 --- /dev/null +++ b/mirage/models/sk-requested-app.ts @@ -0,0 +1,5 @@ +import { Model, belongsTo } from 'miragejs'; + +export default Model.extend({ + app_metadata: belongsTo('sk-app-metadata'), +}); diff --git a/tests/acceptance/storeknox/discovery/requested-apps-test.js b/tests/acceptance/storeknox/discovery/requested-apps-test.js new file mode 100644 index 000000000..9370ad741 --- /dev/null +++ b/tests/acceptance/storeknox/discovery/requested-apps-test.js @@ -0,0 +1,199 @@ +import { module, test } from 'qunit'; +import { visit, findAll, find, triggerEvent } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import dayjs from 'dayjs'; + +import ENUMS from 'irene/enums'; +import { compareInnerHTMLWithIntlTranslation } from 'irene/tests/test-utils'; + +module('Acceptance | storeknox/discovery/requested-apps', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + organization.update({ + features: { + storeknox: true, + }, + }); + + this.setProperties({ currentOrganizationMe }); + }); + + test('it renders empty state when no app exists', async function (assert) { + assert.expect(4); + + this.currentOrganizationMe.update({ is_admin: false }); + + // Server mocks + this.server.get('v2/sk_requested_apps', () => { + return { count: 0, next: null, previous: null, results: [] }; + }); + + await visit('/dashboard/storeknox/discover/requested'); + + assert + .dom('[data-test-storeknoxDiscover-requestedAppsTable-tableEmpty]') + .exists(); + + assert + .dom( + '[data-test-storeknoxDiscover-requestedAppsTable-tableEmptyHeaderText]' + ) + .exists() + .containsText(t('storeknox.noRequestedAppsFound')); + + compareInnerHTMLWithIntlTranslation(assert, { + selector: + '[data-test-storeknoxDiscover-requestedAppsTable-tableEmptyHeaderDescription]', + message: t('storeknox.noRequestedAppsFoundDescription'), + }); + }); + + test.each( + 'it renders requested apps and their correct statuses', + [{ approved: true }, { pending_approval: true }, { rejected: true }], + async function (assert, { approved, rejected }) { + const pending_approval = !approved && !rejected; + + // Models/Test variables + this.currentOrganizationMe.update({ is_admin: false }); + + const skRequestedApp = this.server.create( + 'sk-requested-app', + approved + ? 'withApproval' + : rejected + ? 'withRejection' + : 'withPendingApproval' + ); + + // Server mocks + this.server.get('v2/sk_requested_apps', (schema) => { + const requestedApps = schema.skRequestedApps.all().models.map((a) => ({ + ...a.toJSON(), + app_metadata: a.app_metadata, + })); + + return { + count: requestedApps.length, + next: null, + previous: null, + results: requestedApps, + }; + }); + + await visit('/dashboard/storeknox/discover/requested'); + + const appElementList = findAll( + '[data-test-storeknoxDiscover-requestedAppsTable-row]' + ); + + // Contains the right number of apps + assert.strictEqual(appElementList.length, 1); + + // Sanity check for requested app + const srElement = find( + `[data-test-storeknoxDiscover-requestedAppsTable-rowId='${skRequestedApp.id}']` + ); + + const skRequestedAppMetaData = skRequestedApp.app_metadata; + + assert + .dom(srElement) + .exists() + .containsText(skRequestedAppMetaData.title) + .containsText(skRequestedAppMetaData.dev_email) + .containsText(skRequestedAppMetaData.dev_name); + + assert + .dom('[data-test-applogo-img]', srElement) + .exists() + .hasAttribute('src', skRequestedAppMetaData.icon_url); + + if (skRequestedAppMetaData.platform === ENUMS.PLATFORM.ANDROID) { + assert + .dom( + '[data-test-storeknoxTableColumns-store-playStoreIcon]', + srElement + ) + .exists(); + } + + if (skRequestedAppMetaData.platform === ENUMS.PLATFORM.IOS) { + assert + .dom('[data-test-storeknoxTableColumns-store-iosIcon]', srElement) + .exists(); + } + + if (pending_approval) { + assert + .dom( + '[data-test-storeknoxDiscover-requestedAppsTable-row-waitingForApprovalChip]' + ) + .exists() + .containsText(t('storeknox.waitingForApproval')); + + assert + .dom( + '[data-test-storeknoxDiscover-requestedAppsTable-row-waitingForApprovalChipIcon]' + ) + .exists(); + } else { + assert + .dom( + '[data-test-storeknoxDiscover-requestedAppsTable-row-approvalOrRejectedInfoContainer]' + ) + .exists() + .containsText(t(approved ? 'storeknox.approved' : 'rejected')); + + assert + .dom( + '[data-test-storeknoxDiscover-requestedAppsTable-row-approvalOrRejectedUserInfo]' + ) + .exists() + .containsText(t('by')) + .containsText( + approved ? skRequestedApp.approved_by : skRequestedApp.rejected_by + ); + + // Check for approved/rejected date on tooltip + + const tooltipSelector = + '[data-test-storeknoxDiscover-requestedAppsTable-row-approvalOrRejectedDateTooltipIcon]'; + + const approveOrRejectedDateTooltipTriggerElement = + find(tooltipSelector); + + const tooltipContentSelector = '[data-test-ak-tooltip-content]'; + + assert.dom(tooltipSelector).exists(); + + await triggerEvent( + approveOrRejectedDateTooltipTriggerElement, + 'mouseenter' + ); + + assert + .dom(tooltipContentSelector) + .exists() + .hasText( + dayjs( + approved ? skRequestedApp.approved_on : skRequestedApp.rejected_on + ).format('MMMM D, YYYY, HH:mm') + ); + + await triggerEvent( + approveOrRejectedDateTooltipTriggerElement, + 'mouseleave' + ); + } + } + ); +}); diff --git a/tests/acceptance/storeknox/discovery/results-test.js b/tests/acceptance/storeknox/discovery/results-test.js new file mode 100644 index 000000000..382b011ce --- /dev/null +++ b/tests/acceptance/storeknox/discovery/results-test.js @@ -0,0 +1,777 @@ +import { + visit, + findAll, + find, + triggerEvent, + click, + fillIn, + waitUntil, +} from '@ember/test-helpers'; + +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { t } from 'ember-intl/test-support'; +import { faker } from '@faker-js/faker'; +import { Response } from 'miragejs'; +import Service from '@ember/service'; + +import { setupRequiredEndpoints } from 'irene/tests/helpers/acceptance-utils'; +import { compareInnerHTMLWithIntlTranslation } from 'irene/tests/test-utils'; +import ENUMS from 'irene/enums'; + +// Notification Service +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } + + setDefaultAutoClear() {} +} + +module('Acceptance | storeknox/discovery/results', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + const { organization, currentOrganizationMe } = + await setupRequiredEndpoints(this.server); + + organization.update({ + features: { + storeknox: true, + }, + }); + + this.setProperties({ currentOrganizationMe }); + + this.owner.register('service:notifications', NotificationsStub); + }); + + test.each( + 'it renders with empty state', + [true, false], + async function (assert, is_owner) { + // role set to owner + this.currentOrganizationMe.update({ + is_owner, + }); + + await visit('dashboard/storeknox/discover/result'); + + assert + .dom('[data-test-storeknoxDiscover-header-discoverHeaderText]') + .hasText(t('storeknox.discoverHeader')); + + assert + .dom('[data-test-storeknoxDiscover-header-discoverDescriptionText]') + .hasText(t('storeknox.discoverDescription')); + + const tabItems = [ + { + id: 'discovery-results', + label: t('storeknox.discoveryResults'), + }, + + // Requested apps tab should be displayed for all roles except owners + !this.currentOrganizationMe.is_owner && { + id: 'requested-apps', + label: t('storeknox.requestedApps'), + }, + ]; + + tabItems + .filter(Boolean) + .forEach((tab) => + assert + .dom(`[data-test-storeknox-discovery-tabs='${tab.id}-tab']`) + .exists() + .hasText(tab.label) + ); + + assert + .dom('[data-test-storeknoxDiscover-results-searchQueryInput]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchClearIcon]') + .doesNotExist(); + + assert.dom('[data-test-storeknoxDiscover-results-searchIcon]').exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchTrigger]') + .hasText(t('storeknox.discoverHeader')); + + assert + .dom('[data-test-storeknoxDiscover-results-disclaimerInfoSection]') + .exists() + .containsText(t('storeknox.disclaimer')) + .containsText(t('storeknox.disclaimerHeader')); + + assert + .dom('[data-test-storeknoxDiscover-results-disclaimerInfoWarningIcon]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-viewMoreDisclaimerInfo]') + .hasText(t('viewMore')); + + assert + .dom('[data-test-storeknoxDiscover-resultsEmptyIllustration]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-resultsEmptyContainer]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-resultsEmptyHeaderText]') + .hasText(t('storeknox.searchForApps')); + + assert + .dom('[data-test-storeknoxDiscover-resultsEmptyDescriptionText]') + .hasText(t('storeknox.searchForAppsDescription')); + } + ); + + test('it opens and closes diclaimer modal', async function (assert) { + assert.expect(9); + + await visit('dashboard/storeknox/discover/result'); + + assert + .dom('[data-test-storeknoxDiscover-results-searchQueryInput]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-viewMoreDisclaimerInfo]') + .hasText(t('viewMore')); + + await click('[data-test-storeknoxDiscover-results-viewMoreDisclaimerInfo]'); + + assert + .dom( + '[data-test-storeknoxDiscover-results-disclaimerModalHeaderContainer]' + ) + .hasText(t('storeknox.disclaimer')); + + assert + .dom('[data-test-storeknoxDiscover-results-disclaimerModalWarningIcon]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-disclaimerModalCloseBtn]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-disclaimerModalCloseBtnIcon]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-disclaimerModalHeaderText]') + .hasText(t('storeknox.disclaimerHeader')); + + compareInnerHTMLWithIntlTranslation(assert, { + selector: '[data-test-storeknoxDiscover-results-disclaimerModalBodyText]', + message: t('storeknox.disclaimerBody'), + }); + + // Close modal + await click( + '[data-test-storeknoxDiscover-results-disclaimerModalCloseBtn]' + ); + + assert + .dom( + '[data-test-storeknoxDiscover-results-disclaimerModalHeaderContainer]' + ) + .doesNotExist(); + }); + + test.each( + 'it searches for an application', + [true, false], + async function (assert, is_owner) { + assert.expect(31); + + // role set to owner + this.currentOrganizationMe.update({ + is_owner, + }); + + const searchText = 'example_app'; + + this.server.post('v2/sk_discovery', (schema, req) => { + const { query_str, ...rest } = JSON.parse(req.requestBody); + + assert.strictEqual(query_str, searchText); + + this.set('queryParams', { app_query: query_str }); + + // Create results whose titles include search query + Array.from({ length: 3 }, () => + this.server.create('sk-discovery-result', { + title: `${searchText} ${faker.random.word()}`, + }) + ); + + // Create results whose titles do not include search query + Array.from({ length: 2 }, () => + this.server.create('sk-discovery-result', { + title: faker.random.word(), + }) + ); + + return { + id: 1, + ...rest, + query: { + q: query_str, + }, + }; + }); + + this.server.get('v2/sk_discovery/:id/search_results', (schema) => { + const results = schema.skDiscoveryResults.where((result) => + result.title.includes(searchText) + ).models; + + this.set('searchResults', results); + + return { + count: results.length, + next: null, + previous: null, + results, + }; + }); + + await visit('dashboard/storeknox/discover/result'); + + assert + .dom('[data-test-storeknoxDiscover-results-searchQueryInput]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchClearIcon]') + .doesNotExist(); + + assert.dom('[data-test-storeknoxDiscover-results-searchIcon]').exists(); + + await fillIn( + '[data-test-storeknoxDiscover-results-searchQueryInput]', + searchText + ); + + assert + .dom('[data-test-storeknoxDiscover-results-searchClearIcon]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchIcon]') + .doesNotExist(); + + await click('[data-test-storeknoxDiscover-results-searchTrigger]'); + + assert + .dom('[data-test-storeknoxDiscover-resultsTable-header]') + .exists() + .containsText(t('storeknox.showingResults')) + .containsText(searchText); + + // Sanity check for results + const appElementList = findAll( + '[data-test-storeknoxDiscover-resultsTable-row]' + ); + + // this.searchResults was saved in mock service in this test block + assert.strictEqual(appElementList.length, this.searchResults.length); + + for (let index = 0; index < this.searchResults.length; index++) { + const sr = this.searchResults[index]; + + const srElement = find( + `[data-test-storeknoxDiscover-resultsTable-rowId='${sr.doc_ulid}']` + ); + + assert + .dom(srElement) + .containsText(sr.title) + .containsText(sr.dev_email) + .containsText(sr.dev_name); + + assert + .dom('[data-test-applogo-img]', srElement) + .hasAttribute('src', sr.icon_url); + + if (sr.platform === ENUMS.PLATFORM.ANDROID) { + assert + .dom( + '[data-test-storeknoxTableColumns-store-playStoreIcon]', + srElement + ) + .exists(); + } + + if (sr.platform === ENUMS.PLATFORM.IOS) { + assert + .dom('[data-test-storeknoxTableColumns-store-iosIcon]', srElement) + .exists(); + } + + const addOrSendAddAppReqButton = + '[data-test-storeknoxDiscover-resultsTable-addOrSendAddAppReqButton]'; + + await waitUntil(() => find(addOrSendAddAppReqButton), { + timeout: 500, + }); + + assert.dom(addOrSendAddAppReqButton, srElement).exists(); + + if (is_owner) { + assert + .dom( + '[data-test-storeknoxDiscover-resultsTable-addIcon]', + srElement + ) + .exists(); + } else { + assert + .dom( + '[data-test-storeknoxDiscover-resultsTable-SendAddAppReqIcon]', + srElement + ) + .exists(); + } + } + } + ); + + test.each( + 'it adds/requests add an app to the inventory', + [{ is_owner: true }, { is_owner: false }], + async function (assert, { is_owner }) { + assert.expect(9); + + // role set to owner + this.currentOrganizationMe.update({ + is_owner, + }); + + const searchText = 'example_app'; + + this.server.post('v2/sk_discovery', (schema, req) => { + const { query_str, ...rest } = JSON.parse(req.requestBody); + const app_search_id = 1; + + this.set('queryParams', { app_query: query_str, app_search_id }); + + // Check if query matches search text + assert.strictEqual(query_str, searchText); + + // Create results whose titles which have search query + Array.from({ length: 1 }, () => + this.server.create('sk-discovery-result', { + title: `${searchText} ${faker.random.word()}`, + }) + ); + + return { + id: app_search_id, + ...rest, + query: { + q: query_str, + }, + }; + }); + + this.server.get('v2/sk_discovery/:id/search_results', (schema) => { + const results = schema.skDiscoveryResults.where((result) => { + return result.title.includes(searchText); + }).models; + + this.set('searchResults', results); + + return { + count: results.length, + next: null, + previous: null, + results, + }; + }); + + this.server.post('v2/sk_app', (schema, req) => { + const { doc_ulid, app_discovery_query } = JSON.parse(req.requestBody); + + const app_metadata = this.server.create('sk-app-metadata', { + doc_ulid, + }); + + const skApp = this.server + .create('sk-app', { + app_metadata, + app_status: ENUMS.SK_APP_STATUS.ACTIVE, + + approval_status: is_owner + ? ENUMS.SK_APPROVAL_STATUS.APPROVED + : ENUMS.SK_APPROVAL_STATUS.PENDING_APPROVAL, + }) + .toJSON(); + + return { + ...skApp, + app_discovery_query, + app_metadata: app_metadata.toJSON(), + }; + }); + + await visit('dashboard/storeknox/discover/result'); + + assert + .dom('[data-test-storeknoxDiscover-results-searchQueryInput]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchClearIcon]') + .doesNotExist(); + + assert.dom('[data-test-storeknoxDiscover-results-searchIcon]').exists(); + + await fillIn( + '[data-test-storeknoxDiscover-results-searchQueryInput]', + searchText + ); + + await click('[data-test-storeknoxDiscover-results-searchTrigger]'); + + assert + .dom('[data-test-storeknoxDiscover-resultsTable-header]') + .containsText(t('storeknox.showingResults')) + .containsText(searchText); + + // Add app to inventory + const resultToAdd = this.searchResults[0]; + + const srElement = find( + `[data-test-storeknoxDiscover-resultsTable-rowId='${resultToAdd.doc_ulid}']` + ); + + const addToInventoryTriggerSelector = + '[data-test-storeknoxDiscover-resultsTable-addOrSendAddAppReqButton]'; + + await waitUntil(() => find(addToInventoryTriggerSelector), { + timeout: 300, + }); + + assert.dom(addToInventoryTriggerSelector, srElement).exists(); + + const triggerIconSelector = is_owner + ? '[data-test-storeknoxDiscover-resultsTable-addIcon]' + : '[data-test-storeknoxDiscover-resultsTable-SendAddAppReqIcon]'; + + assert.dom(triggerIconSelector, srElement).exists(); + + await click(addToInventoryTriggerSelector); + + const addOrRequestedIconSelector = + '[data-test-storeknoxDiscover-resultsTable-addedOrRequestedIcon]'; + + // Wait until loading state is finished + await waitUntil(() => find(addOrRequestedIconSelector), { + timeout: 300, + }); + + assert + .dom(addOrRequestedIconSelector, srElement) + .hasClass(is_owner ? /ak-icon-inventory-2/ : /ak-icon-schedule-send/); + } + ); + + test('it renders the correct app request status', async function (assert) { + assert.expect(11); + + const searchText = 'example_app'; + + // Create results whose titles include search query + const resultList = Array.from({ length: 3 }, () => + this.server.create('sk-discovery-result', { + title: `${searchText} ${faker.random.word()}`, + }) + ); + + const resultIdWithPendindApproval = resultList[0].doc_ulid; + const resultIdWithApproval = resultList[1].doc_ulid; + + this.server.post('v2/sk_discovery', (schema, req) => { + const { query_str, ...rest } = JSON.parse(req.requestBody); + const app_search_id = 1; + + this.set('queryParams', { app_query: query_str, app_search_id }); + + // Check if search text and query values are the same + assert.strictEqual(query_str, searchText); + + return { + id: app_search_id, + ...rest, + query: { + q: query_str, + }, + }; + }); + + this.server.get('v2/sk_discovery/:id/search_results', (schema) => { + const results = schema.skDiscoveryResults.all().models; + + return { + count: results.length, + next: null, + previous: null, + results, + }; + }); + + this.server.get('v2/sk_app/check_approval_status', (schema, req) => { + const { doc_ulid } = req.queryParams; + + const is_approved = resultIdWithApproval === doc_ulid; + const is_pending_approval = resultIdWithPendindApproval === doc_ulid; + + if (!is_approved && !is_pending_approval) { + return new Response( + 404, + {}, + { detail: 'App not found in organization inventory.' } + ); + } + + return { + id: faker.number.int(), + + approval_status: is_approved + ? ENUMS.SK_APPROVAL_STATUS.APPROVED + : ENUMS.SK_APPROVAL_STATUS.PENDING_APPROVAL, + + approval_status_display: is_approved ? 'APPROVED' : 'PENDING_APPROVAL', + }; + }); + + await visit('dashboard/storeknox/discover/result'); + + assert + .dom('[data-test-storeknoxDiscover-results-searchQueryInput]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchClearIcon]') + .doesNotExist(); + + assert.dom('[data-test-storeknoxDiscover-results-searchIcon]').exists(); + + await fillIn( + '[data-test-storeknoxDiscover-results-searchQueryInput]', + searchText + ); + + await click('[data-test-storeknoxDiscover-results-searchTrigger]'); + + assert + .dom('[data-test-storeknoxDiscover-resultsTable-header]') + .containsText(t('storeknox.showingResults')) + .containsText(searchText); + + for (let index = 0; index < resultList.length; index++) { + const sr = resultList[index]; + const is_approved = resultIdWithApproval === sr.doc_ulid; + const is_pending_approval = resultIdWithPendindApproval === sr.doc_ulid; + + const srElement = find( + `[data-test-storeknoxDiscover-resultsTable-rowId='${sr.doc_ulid}']` + ); + + if (!is_pending_approval && !is_approved) { + const addToInventoryTriggerSelector = + '[data-test-storeknoxDiscover-resultsTable-addOrSendAddAppReqButton]'; + + await waitUntil(() => find(addToInventoryTriggerSelector), { + timeout: 300, + }); + + assert.dom(addToInventoryTriggerSelector, srElement).exists(); + + assert + .dom( + this.currentOrganizationMe.is_owner + ? '[data-test-storeknoxDiscover-resultsTable-addIcon]' + : '[data-test-storeknoxDiscover-resultsTable-SendAddAppReqIcon]', + srElement + ) + .exists(); + + // Check for tooltip message + const tooltipSelector = + '[data-test-storeknoxDiscover-resultsTable-addedOrRequestedTooltip]'; + + const tooltipContentSelector = '[data-test-ak-tooltip-content]'; + const actionTriggerBtnTooltip = find(tooltipSelector); + + await triggerEvent(actionTriggerBtnTooltip, 'mouseenter'); + + assert + .dom(tooltipContentSelector) + .hasText( + t( + is_approved + ? 'storeknox.appAlreadyExists' + : 'storeknox.appAlreadyRequested' + ) + ); + + await triggerEvent(actionTriggerBtnTooltip, 'mouseleave'); + } else { + const addOrRequestedIconSelector = + '[data-test-storeknoxDiscover-resultsTable-addedOrRequestedIcon]'; + + // Wait until check approval is completed + await waitUntil(() => find(addOrRequestedIconSelector), { + timeout: 300, + }); + + assert + .dom(addOrRequestedIconSelector, srElement) + .hasClass( + is_approved ? /ak-icon-inventory-2/ : /ak-icon-schedule-send/ + ); + } + } + }); + + test('it throws error if app is already added to inventory or pending approval', async function (assert) { + assert.expect(); + + const searchText = 'example_app'; + + const errMessage = + 'An app with this ULID is already in the inventory in APPROVED status'; + + // server mocks + this.server.post('/v2/sk_discovery', (schema, req) => { + const { query_str, ...rest } = JSON.parse(req.requestBody); + const app_search_id = 1; + + this.set('queryParams', { app_query: query_str, app_search_id }); + + // Check if query matches search text + assert.strictEqual(query_str, searchText); + + // Create results whose titles which have search query + Array.from({ length: 1 }, () => + this.server.create('sk-discovery-result', { + title: `${searchText} ${faker.random.word()}`, + }) + ); + + return { + id: app_search_id, + ...rest, + query: { + q: query_str, + }, + }; + }); + + this.server.get('v2/sk_discovery/:id/search_results', (schema) => { + const results = schema.skDiscoveryResults.where((result) => { + return result.title.includes(searchText); + }).models; + + this.set('searchResults', results); + + return { + count: results.length, + next: null, + previous: null, + results, + }; + }); + + this.server.post('v2/sk_app', () => { + return new Response( + 400, + {}, + { + doc_ulid: [errMessage], + } + ); + }); + + // Test Start + await visit('dashboard/storeknox/discover/result'); + + assert + .dom('[data-test-storeknoxDiscover-results-searchQueryInput]') + .exists(); + + assert + .dom('[data-test-storeknoxDiscover-results-searchClearIcon]') + .doesNotExist(); + + assert.dom('[data-test-storeknoxDiscover-results-searchIcon]').exists(); + + await fillIn( + '[data-test-storeknoxDiscover-results-searchQueryInput]', + searchText + ); + + await click('[data-test-storeknoxDiscover-results-searchTrigger]'); + + assert + .dom('[data-test-storeknoxDiscover-resultsTable-header]') + .containsText(t('storeknox.showingResults')) + .containsText(searchText); + + // Add app to inventory + const resultToAdd = this.searchResults[0]; + + const srElement = find( + `[data-test-storeknoxDiscover-resultsTable-rowId='${resultToAdd.doc_ulid}']` + ); + + const addToInventoryTriggerSelector = + '[data-test-storeknoxDiscover-resultsTable-addOrSendAddAppReqButton]'; + + await waitUntil(() => find(addToInventoryTriggerSelector), { + timeout: 300, + }); + + assert.dom(addToInventoryTriggerSelector, srElement).exists(); + + const triggerIconSelector = this.currentOrganizationMe.is_owner + ? '[data-test-storeknoxDiscover-resultsTable-addIcon]' + : '[data-test-storeknoxDiscover-resultsTable-SendAddAppReqIcon]'; + + assert.dom(triggerIconSelector, srElement).exists(); + + await click(addToInventoryTriggerSelector); + + // Wait until loading state is finished + await waitUntil(() => find(addToInventoryTriggerSelector), { + timeout: 5000, + }); + + assert.dom(addToInventoryTriggerSelector, srElement).exists(); + assert.dom(triggerIconSelector, srElement).exists(); + + const notify = this.owner.lookup('service:notifications'); + + assert.strictEqual(notify.errorMsg, errMessage); + }); +});