From c9a6103ae11e25242cfb245a61079b227f7ba84e Mon Sep 17 00:00:00 2001 From: Grace Dong <127385561+i53577@users.noreply.github.com> Date: Fri, 12 May 2023 15:25:57 +0800 Subject: [PATCH 001/122] CXSPA-3391: Change sortType from unit to account to keep consistent with the column name (#17409) --- feature-libs/asm/assets/translations/en/asm.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature-libs/asm/assets/translations/en/asm.ts b/feature-libs/asm/assets/translations/en/asm.ts index 041bf1e7ff0..13024163904 100644 --- a/feature-libs/asm/assets/translations/en/asm.ts +++ b/feature-libs/asm/assets/translations/en/asm.ts @@ -82,8 +82,8 @@ export const asm = { byDateDesc: 'Date (Desc)', byOrderDateAsc: 'Order date (Asc)', byOrderDateDesc: 'Order date (Desc)', - byUnit: 'Unit (Asc)', - byUnitDesc: 'Unit (Desc)', + byUnit: 'Account (Asc)', + byUnitDesc: 'Account (Desc)', }, page: { page: 'Page {{count}}', From cc9485d97bc2a2f8e1f5a72f82350a31610f2084 Mon Sep 17 00:00:00 2001 From: Christoph Hinssen <33626130+ChristophHi@users.noreply.github.com> Date: Mon, 15 May 2023 09:32:42 +0200 Subject: [PATCH 002/122] feat: show message on configurator conflicts resolved (#17404) Refers to CXSPA-3293 --- .../translations/en/configurator-common.ts | 1 + .../form/configurator-form.component.html | 16 +-- .../form/configurator-form.component.spec.ts | 98 ++++++++++++++++++- .../form/configurator-form.component.ts | 89 ++++++++++++++++- ...duct-configurator-vc-interactive.e2e.cy.ts | 34 +++++++ .../helpers/product-configurator-vc.ts | 8 ++ 6 files changed, 232 insertions(+), 14 deletions(-) diff --git a/feature-libs/product-configurator/common/assets/translations/en/configurator-common.ts b/feature-libs/product-configurator/common/assets/translations/en/configurator-common.ts index 1a64dfce843..e6922faed9c 100644 --- a/feature-libs/product-configurator/common/assets/translations/en/configurator-common.ts +++ b/feature-libs/product-configurator/common/assets/translations/en/configurator-common.ts @@ -18,6 +18,7 @@ export const configurator = { resolveConflicts: 'Resolve Conflicts', conflictWarning: 'Conflict must be resolved to continue', updateMessage: 'The configuration is being updated in the background', + conflictsResolved: 'Conflicts have been resolved', showMore: 'show more', showLess: 'show less', items: '{{count}} item', diff --git a/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.html b/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.html index 9bfc3a3ed2e..860a8de543b 100644 --- a/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.html +++ b/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.html @@ -1,12 +1,14 @@ - - + + + + + diff --git a/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.spec.ts b/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.spec.ts index ad58025b9a5..27e0189ad19 100644 --- a/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.spec.ts @@ -9,7 +9,12 @@ import { import { ReactiveFormsModule } from '@angular/forms'; import { RouterState } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; -import { I18nTestingModule, RoutingService } from '@spartacus/core'; +import { + FeatureConfigService, + GlobalMessageService, + I18nTestingModule, + RoutingService, +} from '@spartacus/core'; import { CommonConfigurator, ConfiguratorModelUtils, @@ -88,6 +93,7 @@ let configurationCreateObservable: Observable = EMPTY; let currentGroupObservable: Observable = EMPTY; let isConfigurationLoadingObservable: Observable = EMPTY; +let testVersion: string; class MockRoutingService { getRouterState(): Observable { @@ -162,6 +168,16 @@ class MockLaunchDialogService { openDialogAndSubscribe() {} } +class MockGlobalMessageService { + add(): void {} +} + +class MockFeatureConfigService { + isLevel(version: string): boolean { + return version === testVersion; + } +} + function checkConfigurationObs( routerMarbels: string, configurationServiceMarbels: string, @@ -241,6 +257,7 @@ function mockRouterStateWithQueryParams(queryParams: {}): Observable; @@ -263,7 +280,8 @@ describe('ConfigurationFormComponent', () => { provide: RoutingService, useClass: MockRoutingService, }, - + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { provide: FeatureConfigService, useClass: MockFeatureConfigService }, { provide: ConfiguratorCommonsService, useClass: MockConfiguratorCommonsService, @@ -324,6 +342,12 @@ describe('ConfigurationFormComponent', () => { 'checkConflictSolverDialog' ).and.callThrough(); + globalMessageService = TestBed.inject( + GlobalMessageService as Type + ); + spyOn(globalMessageService, 'add').and.callThrough(); + testVersion = '6.1'; + isConfigurationLoadingObservable = of(false); configExpertModeService = TestBed.inject( @@ -546,4 +570,74 @@ describe('ConfigurationFormComponent', () => { expect(launchDialogService.openDialogAndSubscribe).not.toHaveBeenCalled(); })); }); + + describe('listenForConflictResolution()', () => { + it('should raise message in case a conflict has been resolved', () => { + hasConfigurationConflictsObservable = of(true, false); + createComponentWithData(); + expect(globalMessageService.add).toHaveBeenCalledTimes(1); + }); + + it('should not raise a message in case the configuration has no issues (as we skipped the first submission)', () => { + hasConfigurationConflictsObservable = of(false); + createComponentWithData(); + expect(globalMessageService.add).toHaveBeenCalledTimes(0); + }); + + it('should only emit on status changes', () => { + hasConfigurationConflictsObservable = of( + true, + true, + true, + false, + false, + false + ); + createComponentWithData(); + expect(globalMessageService.add).toHaveBeenCalledTimes(1); + }); + + it('should emit on every status change from conflicting to non-conflicting', () => { + hasConfigurationConflictsObservable = of( + false, + true, + false, + true, + false, + true, + false + ); + createComponentWithData(); + expect(globalMessageService.add).toHaveBeenCalledTimes(3); + }); + }); + + describe('displayConflictResolvedMessage()', () => { + it('should call global message service', () => { + createComponentWithoutData(); + component['displayConflictResolvedMessage'](); + expect(globalMessageService.add).toHaveBeenCalledTimes(1); + }); + + it('should handle non availability of global message service', () => { + createComponentWithoutData(); + component['globalMessageService'] = undefined; + component['displayConflictResolvedMessage'](); + expect(globalMessageService.add).toHaveBeenCalledTimes(0); + }); + + it('should not call global message service if target version is not matched', () => { + createComponentWithoutData(); + testVersion = '6.2'; + component['displayConflictResolvedMessage'](); + expect(globalMessageService.add).toHaveBeenCalledTimes(0); + }); + + it('should handle non availability of feature config service', () => { + createComponentWithoutData(); + component['featureConfigservice'] = undefined; + component['displayConflictResolvedMessage'](); + expect(globalMessageService.add).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.ts b/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.ts index f18cd3a7edc..decdf9fec60 100644 --- a/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.ts +++ b/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.ts @@ -4,25 +4,45 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + Optional, +} from '@angular/core'; import { ConfiguratorRouter, ConfiguratorRouterExtractorService, } from '@spartacus/product-configurator/common'; import { LaunchDialogService, LAUNCH_CALLER } from '@spartacus/storefront'; -import { Observable } from 'rxjs'; -import { delay, filter, switchMap, take } from 'rxjs/operators'; +import { Observable, Subscription } from 'rxjs'; +import { + delay, + distinctUntilChanged, + filter, + skip, + switchMap, + take, +} from 'rxjs/operators'; import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service'; import { ConfiguratorGroupsService } from '../../core/facade/configurator-groups.service'; import { Configurator } from '../../core/model/configurator.model'; import { ConfiguratorExpertModeService } from '../../core/services/configurator-expert-mode.service'; +import { + FeatureConfigService, + GlobalMessageService, + GlobalMessageType, +} from '@spartacus/core'; @Component({ selector: 'cx-configurator-form', templateUrl: './configurator-form.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ConfiguratorFormComponent implements OnInit { +export class ConfiguratorFormComponent implements OnInit, OnDestroy { + protected subscription = new Subscription(); + routerData$: Observable = this.configRouterExtractorService.extractRouterData(); @@ -45,16 +65,75 @@ export class ConfiguratorFormComponent implements OnInit { this.configuratorGroupsService.getCurrentGroup(routerData.owner) ) ); + // TODO (CXSPA-3392): make globalMessageService a required dependency + constructor( + configuratorCommonsService: ConfiguratorCommonsService, + configuratorGroupsService: ConfiguratorGroupsService, + configRouterExtractorService: ConfiguratorRouterExtractorService, + configExpertModeService: ConfiguratorExpertModeService, + launchDialogService: LaunchDialogService, + // eslint-disable-next-line @typescript-eslint/unified-signatures + featureConfigService: FeatureConfigService, + // eslint-disable-next-line @typescript-eslint/unified-signatures + globalMessageService: GlobalMessageService + ); + + /** + * @deprecated since 6.1 + */ + constructor( + configuratorCommonsService: ConfiguratorCommonsService, + configuratorGroupsService: ConfiguratorGroupsService, + configRouterExtractorService: ConfiguratorRouterExtractorService, + configExpertModeService: ConfiguratorExpertModeService, + launchDialogService: LaunchDialogService + ); constructor( protected configuratorCommonsService: ConfiguratorCommonsService, protected configuratorGroupsService: ConfiguratorGroupsService, protected configRouterExtractorService: ConfiguratorRouterExtractorService, protected configExpertModeService: ConfiguratorExpertModeService, - protected launchDialogService: LaunchDialogService + protected launchDialogService: LaunchDialogService, + // TODO:(CXSPA-3392) for next major release remove feature config service + @Optional() protected featureConfigservice?: FeatureConfigService, + @Optional() protected globalMessageService?: GlobalMessageService ) {} + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + protected listenForConflictResolution(): void { + this.subscription.add( + this.routerData$ + .pipe( + switchMap((routerData) => + this.configuratorCommonsService.hasConflicts(routerData.owner) + ), + distinctUntilChanged(), // we are interested only in status changes + skip(1), // we skip the very first emission to avoid the change fron undefined -> no conflicts + filter((hasConflicts) => !hasConflicts) + ) + .subscribe(() => this.displayConflictResolvedMessage()) + ); + } + + protected displayConflictResolvedMessage(): void { + if ( + this.globalMessageService && + (this.featureConfigservice?.isLevel('6.1') ?? false) + ) { + this.globalMessageService.add( + { key: 'configurator.header.conflictsResolved' }, + GlobalMessageType.MSG_TYPE_CONFIRMATION + ); + } + } + ngOnInit(): void { + this.listenForConflictResolution(); + this.routerData$ .pipe( switchMap((routerData) => { diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/product_configurator/product-configurator-vc-interactive.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/product_configurator/product-configurator-vc-interactive.e2e.cy.ts index 562422024e2..ae4e2e9618f 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/product_configurator/product-configurator-vc-interactive.e2e.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/regression/product_configurator/product-configurator-vc-interactive.e2e.cy.ts @@ -498,5 +498,39 @@ context('Product Configuration', () => { ); } }); + it('should display a success message on conflict resolution (CXSPA-2374)', () => { + configurationVc.goToConfigurationPage( + electronicsShop, + testProductMultiLevel + ); + configurationVc.registerConfigurationUpdateRoute(); + configurationVc.clickOnNextBtnAndWait(PROJECTOR); + configurationVc.selectAttributeAndWait( + PROJECTOR_TYPE, + radioGroup, + PROJECTOR_LCD, + commerceRelease.isPricingEnabled + ); + configurationVc.clickOnPreviousBtnAndWait(GENERAL); + configurationVc.clickOnGroupAndWait(3); + + configurationVc.selectConflictingValueAndWait( + GAMING_CONSOLE, + radioGroup, + GAMING_CONSOLE_YES, + 1, + commerceRelease.isPricingEnabled + ); + configurationVc.checkGlobalMessageNotDisplayed(); + configurationVc.deselectConflictingValueAndWait( + GAMING_CONSOLE, + radioGroup, + GAMING_CONSOLE_NO, + commerceRelease.isPricingEnabled + ); + configurationVc.checkGlobalMessageContains( + `Conflicts have been resolved` + ); + }); }); }); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/product-configurator-vc.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/product-configurator-vc.ts index 1a7f2773680..cf2265cc940 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/product-configurator-vc.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/product-configurator-vc.ts @@ -135,6 +135,14 @@ export function checkGlobalMessageNotDisplayed(): void { cy.get('cx-global-message').should('not.be.visible'); } +/** + * Verifies whether the global message is displayed and contains a text + * @param {string} text - We expect this text to appear in the global message + */ +export function checkGlobalMessageContains(text: string): void { + cy.get('cx-global-message').should('contain', text); +} + /** * Clicks on 'Add to Cart' button in catalog list. */ From 831462cf3980c425a06dfc503c3f2e46bdc84c51 Mon Sep 17 00:00:00 2001 From: luoyifeng11 <127297696+luoyifeng11@users.noreply.github.com> Date: Mon, 15 May 2023 17:17:47 +0800 Subject: [PATCH 003/122] CXSPA-3385: Add e2e test for b2b for customer list (#17403) --- .../_customer-selection.component.scss | 6 +++ .../vendor/asm/b2b/customer-list.e2e.cy.ts | 3 +- .../cypress/helpers/asm.ts | 37 ++++++++++++++++ .../cypress/support/cart.commands.ts | 43 ++++++++++++++++++- .../cypress/support/utils/cart.ts | 28 ++++++++++++ 5 files changed, 115 insertions(+), 2 deletions(-) diff --git a/feature-libs/asm/styles/components/_customer-selection.component.scss b/feature-libs/asm/styles/components/_customer-selection.component.scss index 00f7129a8b7..e719f336d9b 100644 --- a/feature-libs/asm/styles/components/_customer-selection.component.scss +++ b/feature-libs/asm/styles/components/_customer-selection.component.scss @@ -13,6 +13,12 @@ margin: 0 0 15px; min-width: auto; + @include forVersion(6.1) { + cx-form-errors p { + color: #db0002; + } + } + .input-contaier { display: flex; .icon-wrapper { diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/asm/b2b/customer-list.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/asm/b2b/customer-list.e2e.cy.ts index 017b396c1f2..fd013d5dab6 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/asm/b2b/customer-list.e2e.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/vendor/asm/b2b/customer-list.e2e.cy.ts @@ -20,13 +20,14 @@ context('Assisted Service Module', () => { Cypress.env('BASE_SITE', ELECTRONICS_BASESITE); }); describe('ASM Customer list', () => { - it('checking custom list features', () => { + it('checking custom list features (CXSPA-1595)', () => { cy.cxConfig({ context: { baseSite: ['powertools-spa'], currency: ['USD'], }, }); + asm.addCartForB2BCustomer(); checkout.visitHomePage('asm=true'); cy.get('cx-asm-main-ui').should('exist'); cy.get('cx-asm-main-ui').should('be.visible'); diff --git a/projects/storefrontapp-e2e-cypress/cypress/helpers/asm.ts b/projects/storefrontapp-e2e-cypress/cypress/helpers/asm.ts index 70009dbf91a..67ea358bd26 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/helpers/asm.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/helpers/asm.ts @@ -25,6 +25,14 @@ import { } from './navigation'; import { generateMail, randomString } from './user'; +export function addCartForB2BCustomer(): void { + const productCode = '1979039'; + cy.login('gi.sun@pronto-hw.com', 'pw4all').then(() => { + const auth = JSON.parse(localStorage.getItem('spartacus⚿⚿auth')); + cy.addProductToB2BCart(productCode, 1, auth.token.access_token); + }); +} + export function listenForAuthenticationRequest(): string { return interceptPost( 'csAgentAuthentication', @@ -253,6 +261,35 @@ export function asmB2bCustomerLists(): void { cy.get('cx-customer-list table').contains('Account'); cy.get('cx-customer-list button.cx-asm-customer-list-btn-cancel').click(); cy.get('cx-customer-list').should('not.exist'); + + cy.log('--> start emulation by click cart'); + asm.asmOpenCustomerList(); + cy.get('cx-customer-list ng-select.customer-list-selector').then( + (selects) => { + let select = selects[0]; + cy.wrap(select) + .click() + .get('ng-dropdown-panel') + .get('.ng-option') + .eq(1) + .then((item) => { + cy.wrap(item).click(); + cy.wait(customerSearchRequestAlias) + .its('response.statusCode') + .should('eq', 200); + }); + } + ); + + cy.get('cx-customer-list table') + .contains('tbody tr', 'Gi Sun') + .closest('tbody tr') + .find('td:nth-child(5)') + .then(($cart) => { + cy.wrap($cart).click(); + cy.get('cx-customer-list').should('not.exist'); + cy.get('cx-add-to-saved-cart').should('exist'); + }); } export function asmB2bCustomerListPagination(): void { diff --git a/projects/storefrontapp-e2e-cypress/cypress/support/cart.commands.ts b/projects/storefrontapp-e2e-cypress/cypress/support/cart.commands.ts index e2b4999099c..3dea7a6568e 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/support/cart.commands.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/support/cart.commands.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { addToCart, createCart } from './utils/cart'; +import { addProductToB2BCart, addToCart, createCart } from './utils/cart'; declare namespace Cypress { interface Chainable { @@ -17,6 +17,20 @@ declare namespace Cypress { ``` */ addToCart: (itemId: string, quantity: string, accessToken: string) => void; + + /** + * Creates a new cart and adds items to B2B Cart + * @memberof Cypress.Chainable + * @example + ``` + cy.addProductToB2BCart(productCode, quantity, accessToken) + ``` + */ + addProductToB2BCart: ( + itemId: string, + quantity: string, + accessToken: string + ) => void; } } @@ -44,3 +58,30 @@ Cypress.Commands.add( }); } ); + +Cypress.Commands.add( + 'addProductToB2BCart', + (productCode: string, quantity: string, accessToken: string) => { + createCart(accessToken).then((response) => { + const cartId = response.body.code; + addProductToB2BCart(cartId, productCode, quantity, accessToken).then( + () => { + Cypress.log({ + name: 'addToCart', + displayName: 'Add to B2B cart', + message: [`🛒 Product(s) added to cart`], + consoleProps: () => { + return { + 'Cart ID': cartId, + 'Product code': productCode, + Quantity: quantity, + }; + }, + }); + + cy.wrap(cartId); + } + ); + }); + } +); diff --git a/projects/storefrontapp-e2e-cypress/cypress/support/utils/cart.ts b/projects/storefrontapp-e2e-cypress/cypress/support/utils/cart.ts index 2c9b18423aa..a47e5d6f244 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/support/utils/cart.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/support/utils/cart.ts @@ -47,3 +47,31 @@ export function addToCart( }, }); } + +export function addProductToB2BCart( + cartCode: string, + productCode: string, + quantity: string, + accessToken: string +) { + const addToCartUrl = `${Cypress.env('API_URL')}/${Cypress.env( + 'OCC_PREFIX' + )}/${Cypress.env('BASE_SITE')}/orgUsers/current/carts/${cartCode}/entries/`; + return cy.request({ + method: 'POST', + url: addToCartUrl, + body: { + orderEntries: [ + { + product: { + code: productCode, + }, + quantity: quantity, + }, + ], + }, + headers: { + Authorization: `bearer ${accessToken}`, + }, + }); +} From 65e6e2ad530580248eef72164fb24625902661fa Mon Sep 17 00:00:00 2001 From: luoyifeng11 <127297696+luoyifeng11@users.noreply.github.com> Date: Tue, 16 May 2023 13:29:07 +0800 Subject: [PATCH 004/122] =?UTF-8?q?CXSPA-3095:=20Adjust=20the=20icon=20in?= =?UTF-8?q?=20the=20alert=20message=20strip=20component=20to=20t=E2=80=A6?= =?UTF-8?q?=20(#17415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature-libs/asm/assets/translations/en/asm.ts | 1 + .../customer-list/customer-list.component.html | 15 +++++++++++---- .../_asm-create-customer-form.component.scss | 11 ++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/feature-libs/asm/assets/translations/en/asm.ts b/feature-libs/asm/assets/translations/en/asm.ts index 13024163904..caa5897b675 100644 --- a/feature-libs/asm/assets/translations/en/asm.ts +++ b/feature-libs/asm/assets/translations/en/asm.ts @@ -91,6 +91,7 @@ export const asm = { next: 'Next', }, noOfCustomers: '{{count}} Customers', + oneCustomer: '1 Customer', noCustomers: 'There are currently no customers in this customer list.', noLists: 'There are currently no customer lists available. Contact your system administrator.', diff --git a/feature-libs/asm/components/customer-list/customer-list.component.html b/feature-libs/asm/components/customer-list/customer-list.component.html index e57aec543cf..3ba825e3664 100644 --- a/feature-libs/asm/components/customer-list/customer-list.component.html +++ b/feature-libs/asm/components/customer-list/customer-list.component.html @@ -439,10 +439,17 @@