diff --git a/core-libs/storefront/cms-components/navigation/navigation/navigation-ui.component.ts b/core-libs/storefront/cms-components/navigation/navigation/navigation-ui.component.ts index a2e7cc866ce..66146589c82 100644 --- a/core-libs/storefront/cms-components/navigation/navigation/navigation-ui.component.ts +++ b/core-libs/storefront/cms-components/navigation/navigation/navigation-ui.component.ts @@ -27,7 +27,8 @@ import { filter, take, } from 'rxjs/operators'; -import { BREAKPOINT, BreakpointService } from '../../../layout'; +import { BREAKPOINT } from '../../../layout/config/layout-config'; +import { BreakpointService } from '../../../layout/breakpoint/breakpoint.service'; import { GenericLinkComponent } from '../../../shared/components/generic-link/generic-link.component'; import { IconComponent } from '../../misc/icon/icon.component'; import { ICON_TYPE } from '../../misc/icon/index'; diff --git a/core-libs/storefront/shared/components/ng-select-a11y/ng-select-a11y.directive.ts b/core-libs/storefront/shared/components/ng-select-a11y/ng-select-a11y.directive.ts index 895de0df36a..c85c348eb75 100644 --- a/core-libs/storefront/shared/components/ng-select-a11y/ng-select-a11y.directive.ts +++ b/core-libs/storefront/shared/components/ng-select-a11y/ng-select-a11y.directive.ts @@ -31,7 +31,8 @@ import { } from '@spartacus/core'; import { filter, merge, take } from 'rxjs'; import { map } from 'rxjs/operators'; -import { BREAKPOINT, BreakpointService } from '../../../layout'; +import { BREAKPOINT } from '../../../layout/config/layout-config'; +import { BreakpointService } from '../../../layout/breakpoint/breakpoint.service'; const ARIA_LABEL = 'aria-label'; const ARIA_HIDDEN = 'aria-hidden'; diff --git a/feature-libs/cart/base/components/cart-shared/cart-item-list/cart-item-list.component.spec.ts b/feature-libs/cart/base/components/cart-shared/cart-item-list/cart-item-list.component.spec.ts index 6101aeeb9d5..98ed00b2137 100644 --- a/feature-libs/cart/base/components/cart-shared/cart-item-list/cart-item-list.component.spec.ts +++ b/feature-libs/cart/base/components/cart-shared/cart-item-list/cart-item-list.component.spec.ts @@ -671,9 +671,13 @@ describe('CartItemListComponent', () => { }); it('should not call _setItems when neither contextRequiresRerender nor isItemsChanged are true', () => { - configureTestingModule().overrideProvider(OutletContextData, { - useValue: { context$ }, - }); + configureTestingModule() + .overrideProvider(OutletContextData, { + useValue: { context$ }, + }) + .overrideProvider(FeatureToggles, { + useValue: { a11yPreventCartItemsFormRedundantRecreation: true }, + }); TestBed.compileComponents(); stubServiceAndCreateComponent(); @@ -682,9 +686,6 @@ describe('CartItemListComponent', () => { // and feature toggle is explicitly enabled (dual-token issue under Vite) component.readonly = mockContext.readonly; fixture.componentRef.setInput('items', mockContext.items); - (component as any)['featureToggles'] = { - a11yPreventCartItemsFormRedundantRecreation: true, - }; const spySetItems = vi.spyOn(component, '_setItems'); component.ngOnInit(); diff --git a/feature-libs/cart/base/core/facade/active-cart.service.spec.ts b/feature-libs/cart/base/core/facade/active-cart.service.spec.ts index 94d1a67dc6a..df598942631 100644 --- a/feature-libs/cart/base/core/facade/active-cart.service.spec.ts +++ b/feature-libs/cart/base/core/facade/active-cart.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { Cart, MultiCartFacade, OrderEntry } from '@spartacus/cart/base/root'; import { + FeatureToggles, getLastValueSync, OCC_CART_ID_CURRENT, OCC_USER_ID_ANONYMOUS, @@ -22,7 +23,10 @@ import { } from 'rxjs'; import { take } from 'rxjs/operators'; import { vi } from 'vitest'; -import { provideMockFeatureToggles } from '@spartacus/core/testing/mock-feature-toggles'; +import { + MockFeatureTogglesController, + provideMockFeatureToggles, +} from '@spartacus/core/testing/mock-feature-toggles'; import { ActiveCartService } from './active-cart.service'; const userId$ = new BehaviorSubject(OCC_USER_ID_ANONYMOUS); @@ -72,9 +76,7 @@ const MockWindowRef = { store[key] = `${value}`; }, removeItem: (key: string): void => { - if (key in store) { - store[key] = undefined; - } + delete store[key]; }, }, isBrowser(): boolean { @@ -343,7 +345,7 @@ describe('ActiveCartService', () => { 'oAuthRedirectCodeFlow' ); - expect(storedOauthFlowKey).toBeUndefined(); + expect(storedOauthFlowKey).toBeNull(); }); }); @@ -743,7 +745,6 @@ describe('ActiveCartService', () => { // context (spartacus⚿). `pendingGuestCartMerge` is the // (protected) key held by ActiveCartStatePersistenceService. const STORAGE_KEY = `spartacus⚿${BASE_SITE}⚿pendingGuestCartMerge`; - beforeEach(() => { winRef?.localStorage?.removeItem(STORAGE_KEY); TestBed.resetTestingModule(); @@ -758,10 +759,13 @@ describe('ActiveCartService', () => { provide: SiteContextParamsService, useValue: { getValues: () => of([BASE_SITE]) }, }, - provideMockFeatureToggles({ - authorizationCodeFlowByDefault: true, - mergeGuestCartOnCodeFlowLogin: true, - }), + { + provide: FeatureToggles, + useValue: { + authorizationCodeFlowByDefault: true, + mergeGuestCartOnCodeFlowLogin: true, + }, + }, ], }); service = TestBed.inject(ActiveCartService); @@ -831,7 +835,7 @@ describe('ActiveCartService', () => { describe('guestCartMerge', () => { it('should add the persisted entries and clear storage without deleting the guest cart', () => { - vi.spyOn(multiCartFacade, 'deleteCart'); + vi.spyOn(multiCartFacade, 'deleteCart').mockImplementation(() => {}); vi.spyOn(service as any, 'addEntriesGuestMerge').mockImplementation( () => {} ); diff --git a/feature-libs/cart/tsconfig.spec.json b/feature-libs/cart/tsconfig.spec.json index c87cb58ca00..876bc73f1d8 100644 --- a/feature-libs/cart/tsconfig.spec.json +++ b/feature-libs/cart/tsconfig.spec.json @@ -40,7 +40,7 @@ ] } }, - "files": ["setup-test.ts"], + "files": ["../../testing/setup-vitest.ts"], "include": [ "**/*.ts", "../../core-libs/core/src/**/*.ts", diff --git a/feature-libs/organization/account-summary/components/details/document/account-summary-document.component.spec.ts b/feature-libs/organization/account-summary/components/details/document/account-summary-document.component.spec.ts index 97dfe09fe0a..0d1e926de8b 100644 --- a/feature-libs/organization/account-summary/components/details/document/account-summary-document.component.spec.ts +++ b/feature-libs/organization/account-summary/components/details/document/account-summary-document.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -33,7 +34,6 @@ import { DocumentStatus, FilterByOptions, } from '@spartacus/organization/account-summary/root'; -import createSpy = jasmine.createSpy; import { RouterModule } from '@angular/router'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; @@ -89,7 +89,7 @@ class MockAccountSummaryFacade implements Partial { } class MockFileDownloadService { - download = createSpy('MockFileDownloadService.download Spy'); + download = vi.fn(); } class MockLanguageService { @@ -169,8 +169,8 @@ describe('AccountSummaryDocumentComponent', () => { it('Should change page and sort', () => { // Spy functions to ensure new documents are being fetched - spyOn(component, 'updateQueryParams').and.callThrough(); - spyOn(accountSummaryFacade, 'getDocumentList').and.callThrough(); + vi.spyOn(component, 'updateQueryParams'); + vi.spyOn(accountSummaryFacade, 'getDocumentList'); // By default page will be 0 expect(component._queryParams.page).toEqual(0); @@ -212,8 +212,8 @@ describe('AccountSummaryDocumentComponent', () => { it('should change filters', () => { // Spy functions to ensure new documents are being fetched - spyOn(component, 'updateQueryParams').and.callThrough(); - spyOn(accountSummaryFacade, 'getDocumentList').and.callThrough(); + vi.spyOn(component, 'updateQueryParams'); + vi.spyOn(accountSummaryFacade, 'getDocumentList'); // Change the filters const status = DocumentStatus.CLOSED; @@ -258,7 +258,7 @@ describe('AccountSummaryDocumentComponent', () => { // Call addNamesToSortModel with two sort options const sorts: Array = [{ code: 'abc' }, { code: 'def' }]; - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); component['addNamesToSortModel'](sorts); // Expect that translate was called twice @@ -279,25 +279,25 @@ describe('AccountSummaryDocumentComponent', () => { const tableHeaders = tableElement.queryAll(By.css('th')); expect(tableHeaders?.length).toEqual(8); - expect(tableHeaders[0].properties.innerText).toEqual( + expect(tableHeaders[0].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.id' ); - expect(tableHeaders[1].properties.innerText).toEqual( + expect(tableHeaders[1].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.type' ); - expect(tableHeaders[2].properties.innerText).toEqual( + expect(tableHeaders[2].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.date' ); - expect(tableHeaders[3].properties.innerText).toEqual( + expect(tableHeaders[3].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.dueDate' ); - expect(tableHeaders[4].properties.innerText).toEqual( + expect(tableHeaders[4].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.originalAmount' ); - expect(tableHeaders[5].properties.innerText).toEqual( + expect(tableHeaders[5].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.openAmount' ); - expect(tableHeaders[6].properties.innerText).toEqual( + expect(tableHeaders[6].nativeElement.textContent?.trim()).toEqual( 'orgAccountSummary.document.status' ); expect(tableHeaders[7].children[0].attributes.title).toEqual( @@ -323,31 +323,31 @@ describe('AccountSummaryDocumentComponent', () => { expect(tableCells?.length).toEqual(8); - expect(tableCells[0].nativeElement.innerText).toEqual( + expect(tableCells[0].nativeElement.textContent?.trim()).toEqual( mockAccountSummaryList.orgDocuments?.[rowNumber]?.id ); - expect(tableCells[1].nativeElement.innerText).toEqual( + expect(tableCells[1].nativeElement.textContent?.trim()).toEqual( mockAccountSummaryList.orgDocuments?.[rowNumber]?.orgDocumentType?.name ); - expect(isDate(tableCells[2].nativeElement.innerText)).toEqual( + expect(isDate(tableCells[2].nativeElement.textContent?.trim())).toEqual( !!mockAccountSummaryList.orgDocuments?.[rowNumber]?.createdAtDate ); - expect(isDate(tableCells[3].nativeElement.innerText)).toEqual( + expect(isDate(tableCells[3].nativeElement.textContent?.trim())).toEqual( !!mockAccountSummaryList.orgDocuments?.[rowNumber]?.dueAtDate ); - expect(tableCells[4].nativeElement.innerText).toEqual( + expect(tableCells[4].nativeElement.textContent?.trim()).toEqual( mockAccountSummaryList.orgDocuments?.[rowNumber]?.formattedAmount ); - expect(tableCells[5].nativeElement.innerText).toEqual( + expect(tableCells[5].nativeElement.textContent?.trim()).toEqual( mockAccountSummaryList.orgDocuments?.[rowNumber]?.formattedOpenAmount ); - expect(tableCells[6].nativeElement.innerText).toEqual( + expect(tableCells[6].nativeElement.textContent?.trim()).toEqual( `orgAccountSummary.statuses.${mockAccountSummaryList.orgDocuments?.[rowNumber]?.status}` ); @@ -363,12 +363,12 @@ describe('AccountSummaryDocumentComponent', () => { (doc) => doc?.attachments?.length && doc?.attachments?.length > 0 ) || {}; - spyOn(accountSummaryFacade, 'getDocumentAttachment').and.returnValue( + vi.spyOn(accountSummaryFacade, 'getDocumentAttachment').mockReturnValue( of(blob) ); const fakeUrl = 'blob:http://localhost:9877/50d43852-5f76-41e0-bb36-599d4b99af07'; - spyOn(URL, 'createObjectURL').and.returnValue(fakeUrl); + vi.spyOn(URL, 'createObjectURL').mockReturnValue(fakeUrl); component.downloadAttachment( documentWithAttachment.id, diff --git a/feature-libs/organization/account-summary/components/details/document/filter/account-summary-document-filter.component.spec.ts b/feature-libs/organization/account-summary/components/details/document/filter/account-summary-document-filter.component.spec.ts index b8ae4dcf4c5..a9e9aca6b05 100644 --- a/feature-libs/organization/account-summary/components/details/document/filter/account-summary-document-filter.component.spec.ts +++ b/feature-libs/organization/account-summary/components/details/document/filter/account-summary-document-filter.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, CUSTOM_ELEMENTS_SCHEMA, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; @@ -149,21 +150,21 @@ describe('AccountSummaryDocumentFilterComponent', () => { }); it('should test Filter By selector', () => { - const eventSpy = spyOn(component.filterListEvent, 'emit'); - const resetSpy = spyOn(component, 'resetForm').and.callThrough(); + const eventSpy = vi.spyOn(component.filterListEvent, 'emit'); + const resetSpy = vi.spyOn(component, 'resetForm'); let filterByValue: string | undefined; let startRange: string | undefined; let endRange: string | undefined; const pressSearch = () => { - eventSpy.calls.reset(); + eventSpy.mockClear(); const searchButton = fixture.debugElement.query(By.css('.btn-primary')); searchButton?.nativeElement.click(); fixture.detectChanges(); }; const pressClear = () => { - eventSpy.calls.reset(); + eventSpy.mockClear(); const clearButton = fixture.debugElement.query(By.css('.clear-btn')); clearButton?.nativeElement.click(); fixture.detectChanges(); @@ -428,7 +429,7 @@ describe('AccountSummaryDocumentFilterComponent', () => { // Press clear button and expect default values and search to be triggered expect(resetSpy).toHaveBeenCalledTimes(6); - resetSpy.calls.reset(); + resetSpy.mockClear(); pressClear(); expect(resetSpy).toHaveBeenCalledWith(true); formItems = filterFormItems(); diff --git a/feature-libs/organization/account-summary/components/details/header/account-summary-header.component.spec.ts b/feature-libs/organization/account-summary/components/details/header/account-summary-header.component.spec.ts index f33adf1da6d..98cff30ce64 100644 --- a/feature-libs/organization/account-summary/components/details/header/account-summary-header.component.spec.ts +++ b/feature-libs/organization/account-summary/components/details/header/account-summary-header.component.spec.ts @@ -1,10 +1,6 @@ +import { vi } from 'vitest'; import { Component, DebugElement, Input } from '@angular/core'; -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Observable, of } from 'rxjs'; @@ -74,16 +70,18 @@ describe('AccountSummaryHeaderComponent', () => { fixture.detectChanges(); }); - it('should create', fakeAsync(() => { - tick(); + it('should create', async () => { + vi.useFakeTimers(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); fixture.detectChanges(); expect(component).toBeTruthy(); - })); + }); it('should get id card', () => { const title = 'mock_id'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getIdCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -97,7 +95,7 @@ describe('AccountSummaryHeaderComponent', () => { it('should get name card', () => { const title = 'mock_name'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getNameCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -121,7 +119,7 @@ describe('AccountSummaryHeaderComponent', () => { address.formattedAddress, address.country?.name, ]; - spyOn(translationService, 'translate').and.returnValue(of('title')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('title')); // Call function and expect that it calls translation and return correct card component.getAddressCardContent(address).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -135,7 +133,7 @@ describe('AccountSummaryHeaderComponent', () => { it('should get creditRep card', () => { const title = 'mock_rep'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getCreditRepCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -149,7 +147,7 @@ describe('AccountSummaryHeaderComponent', () => { it('should get creditLine card', () => { const title = 'mock_credit'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getCreditLineCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -163,7 +161,7 @@ describe('AccountSummaryHeaderComponent', () => { it('should get currentBalance card', () => { const title = 'mock_balance'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getCurrentBalanceCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -177,7 +175,7 @@ describe('AccountSummaryHeaderComponent', () => { it('should get openBalance card', () => { const title = 'mock_balance'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getOpenBalanceCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -191,7 +189,7 @@ describe('AccountSummaryHeaderComponent', () => { it('should get pastDueBalance card', () => { const title = 'mock_balance'; const text = 'mock_text'; - spyOn(translationService, 'translate').and.returnValue(of(title)); + vi.spyOn(translationService, 'translate').mockReturnValue(of(title)); // Call function and expect that it calls translation and return correct card component.getPastDueBalanceCardContent(text).subscribe((result) => { expect(translationService.translate).toHaveBeenCalledWith( @@ -218,8 +216,12 @@ describe('AccountSummaryHeaderComponent', () => { label: string, value: string ) => { - expect(container?.nativeElement?.firstChild?.innerText).toEqual(label); - expect(container?.nativeElement?.lastChild?.innerText).toEqual(value); + expect(container?.nativeElement?.firstChild?.textContent?.trim()).toEqual( + label + ); + expect(container?.nativeElement?.lastChild?.textContent?.trim()).toEqual( + value + ); }; const cards = fixture.debugElement.queryAll(By.css('cx-card')); diff --git a/feature-libs/organization/account-summary/components/services/account-summary-item.service.spec.ts b/feature-libs/organization/account-summary/components/services/account-summary-item.service.spec.ts index 22f3271aa46..9a6a07bf301 100644 --- a/feature-libs/organization/account-summary/components/services/account-summary-item.service.spec.ts +++ b/feature-libs/organization/account-summary/components/services/account-summary-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { B2BUnit, RoutingService } from '@spartacus/core'; import { @@ -44,7 +45,7 @@ describe('AccountSummaryItemService', () => { }); it('should launch account summary detail route with unit uid', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.launchDetails(testB2BUnit); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'orgAccountSummaryDetails', diff --git a/feature-libs/organization/account-summary/components/services/account-summary-unit-list.service.spec.ts b/feature-libs/organization/account-summary/components/services/account-summary-unit-list.service.spec.ts index e762932d25d..3ad4b60d2d9 100644 --- a/feature-libs/organization/account-summary/components/services/account-summary-unit-list.service.spec.ts +++ b/feature-libs/organization/account-summary/components/services/account-summary-unit-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { StoreModule } from '@ngrx/store'; @@ -14,13 +15,11 @@ import { BehaviorSubject, Observable, of } from 'rxjs'; import { AccountSummaryUnitListService } from './account-summary-unit-list.service'; import * as _augmented from '../model/augmented.model'; -import createSpy = jasmine.createSpy; - const treeToggle$ = new BehaviorSubject({}); class MockUnitTreeService { treeToggle$ = treeToggle$.asObservable(); - initialize = createSpy('initialize'); - isExpanded = createSpy('isExpanded').and.returnValue(false); + initialize = vi.fn(); + isExpanded = vi.fn().mockReturnValue(false); } class MockUnitService { diff --git a/feature-libs/organization/account-summary/core/account-summary-page-meta.resolver.spec.ts b/feature-libs/organization/account-summary/core/account-summary-page-meta.resolver.spec.ts index ab3310d4d6e..acb0447386f 100644 --- a/feature-libs/organization/account-summary/core/account-summary-page-meta.resolver.spec.ts +++ b/feature-libs/organization/account-summary/core/account-summary-page-meta.resolver.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { BreadcrumbMeta, @@ -29,7 +30,7 @@ const accountSummariesBreadcrumb = { }; class MockSemanticPathService implements Partial { - get = jasmine.createSpy('get').and.returnValue(testOrganizationUrl); + get = vi.fn().mockReturnValue(testOrganizationUrl); } const testHomeBreadcrumb: BreadcrumbMeta = { label: 'Test Home', link: '/' }; @@ -86,7 +87,7 @@ describe('AccountSummaryPageMetaResolver', () => { describe('resolveBreadcrumbs', () => { describe('when on the Account Summary units list page', () => { beforeEach(() => { - spyOn(routingService, 'getRouterState').and.returnValue( + vi.spyOn(routingService, 'getRouterState').mockReturnValue( of({ state: { semanticRoute: 'orgAccountSummary' } } as any) ); }); @@ -103,11 +104,11 @@ describe('AccountSummaryPageMetaResolver', () => { }; beforeEach(() => { - spyOn(routingService, 'getRouterState').and.returnValue( + vi.spyOn(routingService, 'getRouterState').mockReturnValue( of({ state: { semanticRoute: 'orgAccountSummaryDetails' } } as any) ); - spyOn(contentPageMetaResolver, 'resolveBreadcrumbs').and.returnValue( + vi.spyOn(contentPageMetaResolver, 'resolveBreadcrumbs').mockReturnValue( of([testHomeBreadcrumb, accountSummaryDetailsBreadcrumb]) ); }); diff --git a/feature-libs/organization/account-summary/core/connectors/account-summary.connector.spec.ts b/feature-libs/organization/account-summary/core/connectors/account-summary.connector.spec.ts index 625582f6d99..b7be4391a25 100644 --- a/feature-libs/organization/account-summary/core/connectors/account-summary.connector.spec.ts +++ b/feature-libs/organization/account-summary/core/connectors/account-summary.connector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { @@ -9,7 +10,6 @@ import { } from '../../root/model'; import { AccountSummaryAdapter } from './account-summary.adapter'; import { AccountSummaryConnector } from './account-summary.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId'; const orgUnitId = 'orgUnit'; @@ -63,16 +63,10 @@ const accountSummaryDocumentsResult: AccountSummaryList = { const accountSummaryAttachmentFile = new Blob(); class MockAccountSummaryAdapter implements AccountSummaryAdapter { - getDocumentAttachment = createSpy('getDocumentAttachment').and.returnValue( - accountSummaryAttachmentFile - ); + getDocumentAttachment = vi.fn().mockReturnValue(accountSummaryAttachmentFile); - getAccountSummary = createSpy('getAccountSummary').and.returnValue( - of(accountSummaryResult) - ); - getDocumentList = createSpy('getDocumentList').and.returnValue( - of(accountSummaryDocumentsResult) - ); + getAccountSummary = vi.fn().mockReturnValue(of(accountSummaryResult)); + getDocumentList = vi.fn().mockReturnValue(of(accountSummaryDocumentsResult)); } describe('AccountSummaryConnector', () => { diff --git a/feature-libs/organization/account-summary/core/facade/account-summary.service.spec.ts b/feature-libs/organization/account-summary/core/facade/account-summary.service.spec.ts index 74a7c267783..86b45e002e0 100644 --- a/feature-libs/organization/account-summary/core/facade/account-summary.service.spec.ts +++ b/feature-libs/organization/account-summary/core/facade/account-summary.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { OCC_USER_ID_CURRENT, @@ -15,7 +16,6 @@ import { import { BehaviorSubject, Observable, of } from 'rxjs'; import { AccountSummaryConnector } from '../connectors'; import { AccountSummaryService } from './account-summary.service'; -import createSpy = jasmine.createSpy; const routerStateSubject = new BehaviorSubject({ state: { @@ -25,9 +25,7 @@ const routerStateSubject = new BehaviorSubject({ } as unknown as RouterState); class MockRoutingService implements Partial { - getRouterState = createSpy().and.returnValue( - routerStateSubject.asObservable() - ); + getRouterState = vi.fn().mockReturnValue(routerStateSubject.asObservable()); } class MockUserIdService implements Partial { @@ -97,17 +95,13 @@ const accountSummaryDocumentsResult: AccountSummaryList = { const accountSummaryDocumentBlob = new Blob([], { type: 'application/pdf' }); class MockAccountSummaryConnector implements Partial { - getAccountSummary = createSpy( - 'MockAccountSummaryConnector.getAccountSummary Spy' - ).and.returnValue(of(accountSummaryResult)); + getAccountSummary = vi.fn().mockReturnValue(of(accountSummaryResult)); - getDocumentList = createSpy( - 'MockAccountSummaryConnector.getDocumentList Spy' - ).and.returnValue(of(accountSummaryDocumentsResult)); + getDocumentList = vi.fn().mockReturnValue(of(accountSummaryDocumentsResult)); - getDocumentAttachment = createSpy( - 'MockAccountSummaryConnector.getDocumentAttachment Spy' - ).and.returnValue(of(new Blob([], { type: 'application/pdf' }))); + getDocumentAttachment = vi + .fn() + .mockReturnValue(of(new Blob([], { type: 'application/pdf' }))); } describe('AccountSummaryService', () => { diff --git a/feature-libs/organization/account-summary/occ/adapters/occ-account-summary.adapter.spec.ts b/feature-libs/organization/account-summary/occ/adapters/occ-account-summary.adapter.spec.ts index 8dd73642b74..f0437e4c188 100644 --- a/feature-libs/organization/account-summary/occ/adapters/occ-account-summary.adapter.spec.ts +++ b/feature-libs/organization/account-summary/occ/adapters/occ-account-summary.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -59,8 +60,8 @@ describe('OccAccountSummaryAdapter', () => { converterService = TestBed.inject(ConverterService); occEndpointService = TestBed.inject(OccEndpointsService); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(occEndpointService, 'buildUrl').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(occEndpointService, 'buildUrl'); }); afterEach(() => { diff --git a/feature-libs/organization/account-summary/root/http-interceptors/blob-error.interceptor.spec.ts b/feature-libs/organization/account-summary/root/http-interceptors/blob-error.interceptor.spec.ts index 60fe11dfe8b..08f97f7c944 100644 --- a/feature-libs/organization/account-summary/root/http-interceptors/blob-error.interceptor.spec.ts +++ b/feature-libs/organization/account-summary/root/http-interceptors/blob-error.interceptor.spec.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { vi } from 'vitest'; import { HTTP_INTERCEPTORS, HttpClient, @@ -54,8 +55,8 @@ describe('BlobErrorInterceptor', () => { windowRef = TestBed.inject(WindowRef); }); - it(`Should extract json from errors wrapped in blob`, (done) => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + it(`Should extract json from errors wrapped in blob`, async () => { + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); http .get('/occ', { responseType: 'blob' as 'json' }) @@ -77,11 +78,10 @@ describe('BlobErrorInterceptor', () => { }); expect(windowRef.isBrowser).toHaveBeenCalled(); - done(); }); - it(`Should extract json from errors wrapped in blob`, (done) => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + it(`Should extract json from errors wrapped in blob`, async () => { + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); http .get('/occ', { responseType: 'blob' as 'json' }) @@ -95,6 +95,5 @@ describe('BlobErrorInterceptor', () => { mockReq.flush(error); expect(windowRef.isBrowser).not.toHaveBeenCalled(); - done(); }); }); diff --git a/feature-libs/organization/administration/components/budget/cost-centers/budget-cost-center-list.service.spec.ts b/feature-libs/organization/administration/components/budget/cost-centers/budget-cost-center-list.service.spec.ts index 29e2ed52ba7..4b26e9a4ca5 100644 --- a/feature-libs/organization/administration/components/budget/cost-centers/budget-cost-center-list.service.spec.ts +++ b/feature-libs/organization/administration/components/budget/cost-centers/budget-cost-center-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { CostCenter, EntitiesModel } from '@spartacus/core'; @@ -88,13 +89,13 @@ describe('BudgetCostCenterListService', () => { }); it('should filter selected cost-centers', () => { - spyOn(budgetService, 'getCostCenters').and.returnValue( + vi.spyOn(budgetService, 'getCostCenters').mockReturnValue( of(mockCostCenterEntities2) ); let result: EntitiesModel; service.getData().subscribe((table) => (result = table)); expect(result.values.length).toEqual(2); - expect(result.values).not.toContain({ + expect(result.values).not.toContainEqual({ code: 'second', }); }); diff --git a/feature-libs/organization/administration/components/budget/details/budget-details.component.spec.ts b/feature-libs/organization/administration/components/budget/details/budget-details.component.spec.ts index cc5d2bf44d9..72b88289b0f 100644 --- a/feature-libs/organization/administration/components/budget/details/budget-details.component.spec.ts +++ b/feature-libs/organization/administration/components/budget/details/budget-details.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; import { @@ -16,13 +17,12 @@ import { CardTestingModule } from '../../shared/card/card.testing.module'; import { ItemService } from '../../shared/item.service'; import { MessageTestingModule } from '../../shared/message/message.testing.module'; import { BudgetDetailsComponent } from './budget-details.component'; -import createSpy = jasmine.createSpy; const mockCode = 'b1'; class MockBudgetItemService implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); current$ = of({}); } diff --git a/feature-libs/organization/administration/components/budget/form/budget-form.component.spec.ts b/feature-libs/organization/administration/components/budget/form/budget-form.component.spec.ts index 30726210ce5..5f77d3e481a 100644 --- a/feature-libs/organization/administration/components/budget/form/budget-form.component.spec.ts +++ b/feature-libs/organization/administration/components/budget/form/budget-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { @@ -136,9 +137,9 @@ describe('BudgetFormComponent', () => { currencyService = TestBed.inject(CurrencyService); b2bUnitService = TestBed.inject(OrgUnitService); - spyOn(currencyService, 'getAll').and.callThrough(); - spyOn(b2bUnitService, 'getActiveUnitList').and.callThrough(); - spyOn(b2bUnitService, 'loadList').and.callThrough(); + vi.spyOn(currencyService, 'getAll'); + vi.spyOn(b2bUnitService, 'getActiveUnitList'); + vi.spyOn(b2bUnitService, 'loadList'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/budget/services/budget-item.service.spec.ts b/feature-libs/organization/administration/components/budget/services/budget-item.service.spec.ts index 121d8d8ac36..a1050a4e9c7 100644 --- a/feature-libs/organization/administration/components/budget/services/budget-item.service.spec.ts +++ b/feature-libs/organization/administration/components/budget/services/budget-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RoutingService } from '@spartacus/core'; @@ -11,7 +12,6 @@ import { EMPTY, Observable, of } from 'rxjs'; import { BudgetFormService } from '../form/budget-form.service'; import { BudgetItemService } from './budget-item.service'; import { CurrentBudgetService } from './current-budget.service'; -import createSpy = jasmine.createSpy; const mockCode = 'b1'; @@ -40,7 +40,7 @@ class MockBudgetService { class MockBudgetFormService {} class MockCurrentBudgetService { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } @@ -68,20 +68,20 @@ describe('BudgetItemService', () => { }); it('should load budget', () => { - spyOn(budgetService, 'get').and.callThrough(); + vi.spyOn(budgetService, 'get'); service.load('123').subscribe(); expect(budgetService.get).toHaveBeenCalledWith('123'); }); it('should load budget on each request', () => { - spyOn(budgetService, 'loadBudget').and.callThrough(); + vi.spyOn(budgetService, 'loadBudget'); service.load('123').subscribe(); expect(budgetService.loadBudget).toHaveBeenCalledWith('123'); }); it('should update existing budget', () => { - spyOn(budgetService, 'update').and.callThrough(); - spyOn(budgetService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(budgetService, 'update'); + vi.spyOn(budgetService, 'getLoadingStatus'); expect(service.save(form, 'existingCode')).toEqual(mockItemStatus); expect(budgetService.update).toHaveBeenCalledWith('existingCode', { @@ -92,8 +92,8 @@ describe('BudgetItemService', () => { }); it('should create new budget', () => { - spyOn(budgetService, 'create').and.callThrough(); - spyOn(budgetService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(budgetService, 'create'); + vi.spyOn(budgetService, 'getLoadingStatus'); expect(service.save(form)).toEqual(mockItemStatus); expect(budgetService.create).toHaveBeenCalledWith({ @@ -105,7 +105,7 @@ describe('BudgetItemService', () => { it('should launch budget detail route', () => { const routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.launchDetails({ name: 'foo bar' }); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'orgBudgetDetails', diff --git a/feature-libs/organization/administration/components/budget/services/current-budget.service.spec.ts b/feature-libs/organization/administration/components/budget/services/current-budget.service.spec.ts index 237a15e0dc1..c37afe0728a 100644 --- a/feature-libs/organization/administration/components/budget/services/current-budget.service.spec.ts +++ b/feature-libs/organization/administration/components/budget/services/current-budget.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { RoutingService } from '@spartacus/core'; import { BudgetService } from '@spartacus/organization/administration/core'; @@ -46,14 +47,14 @@ describe('CurrentBudgetService', () => { describe('model$', () => { it('should load budget', () => { - spyOn(budgetService, 'get').and.callThrough(); + vi.spyOn(budgetService, 'get'); service.item$.subscribe(); mockParams.next({ [ROUTE_PARAMS.budgetCode]: '123' }); expect(budgetService.get).toHaveBeenCalledWith('123'); }); it('should not load budget', () => { - spyOn(budgetService, 'get').and.callThrough(); + vi.spyOn(budgetService, 'get'); service.item$.subscribe(); mockParams.next({ foo: 'bar' }); expect(budgetService.get).not.toHaveBeenCalled(); diff --git a/feature-libs/organization/administration/components/cost-center/budgets/cost-center-budget-list.service.spec.ts b/feature-libs/organization/administration/components/cost-center/budgets/cost-center-budget-list.service.spec.ts index 7457eb83f88..c11dbe17a2b 100644 --- a/feature-libs/organization/administration/components/cost-center/budgets/cost-center-budget-list.service.spec.ts +++ b/feature-libs/organization/administration/components/cost-center/budgets/cost-center-budget-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel } from '@spartacus/core'; @@ -96,8 +97,8 @@ describe('CostCenterBudgetListService', () => { }); it('should assign budget', () => { - spyOn(costCenterService, 'assignBudget').and.callThrough(); - spyOn(budgetService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(costCenterService, 'assignBudget'); + vi.spyOn(budgetService, 'getLoadingStatus'); expect(service.assign(costCenterCode, budgetCode)).toEqual(mockItemStatus); expect(costCenterService.assignBudget).toHaveBeenCalledWith( @@ -108,8 +109,8 @@ describe('CostCenterBudgetListService', () => { }); it('should unassign budget', () => { - spyOn(costCenterService, 'unassignBudget').and.callThrough(); - spyOn(budgetService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(costCenterService, 'unassignBudget'); + vi.spyOn(budgetService, 'getLoadingStatus'); expect(service.unassign(costCenterCode, budgetCode)).toEqual( mockItemStatus diff --git a/feature-libs/organization/administration/components/cost-center/details/cost-center-details.component.spec.ts b/feature-libs/organization/administration/components/cost-center/details/cost-center-details.component.spec.ts index 4d8aeb924c4..b8b1a863474 100644 --- a/feature-libs/organization/administration/components/cost-center/details/cost-center-details.component.spec.ts +++ b/feature-libs/organization/administration/components/cost-center/details/cost-center-details.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; @@ -24,13 +25,12 @@ import { ItemService } from '../../shared/item.service'; import { MessageTestingModule } from '../../shared/message/message.testing.module'; import { MessageService } from '../../shared/message/services/message.service'; import { CostCenterDetailsComponent } from './cost-center-details.component'; -import createSpy = jasmine.createSpy; const mockCode = 'c1'; class MockItemService implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } diff --git a/feature-libs/organization/administration/components/cost-center/form/cost-center-form.component.spec.ts b/feature-libs/organization/administration/components/cost-center/form/cost-center-form.component.spec.ts index 1f27fee1654..8f6c352a590 100644 --- a/feature-libs/organization/administration/components/cost-center/form/cost-center-form.component.spec.ts +++ b/feature-libs/organization/administration/components/cost-center/form/cost-center-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, @@ -108,9 +109,9 @@ describe('CostCenterFormComponent', () => { currencyService = TestBed.inject(CurrencyService); b2bUnitService = TestBed.inject(OrgUnitService); - spyOn(currencyService, 'getAll').and.callThrough(); - spyOn(b2bUnitService, 'getActiveUnitList').and.callThrough(); - spyOn(b2bUnitService, 'loadList').and.callThrough(); + vi.spyOn(currencyService, 'getAll'); + vi.spyOn(b2bUnitService, 'getActiveUnitList'); + vi.spyOn(b2bUnitService, 'loadList'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/cost-center/services/cost-center-list.service.spec.ts b/feature-libs/organization/administration/components/cost-center/services/cost-center-list.service.spec.ts index 3ab7de53cd5..85b471642ad 100644 --- a/feature-libs/organization/administration/components/cost-center/services/cost-center-list.service.spec.ts +++ b/feature-libs/organization/administration/components/cost-center/services/cost-center-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { CostCenter, EntitiesModel, FeatureToggles } from '@spartacus/core'; @@ -92,7 +93,7 @@ describe('CostCenterListService', () => { }); it('should get empty table with 10 rows', () => { - spyOn(costCenterService, 'getList').and.returnValue(of(undefined)); + vi.spyOn(costCenterService, 'getList').mockReturnValue(of(undefined)); let result: EntitiesModel; service.getData().subscribe((table) => (result = table)); expect(result.values.length).toBe(10); diff --git a/feature-libs/organization/administration/components/permission/details/permission-details.component.spec.ts b/feature-libs/organization/administration/components/permission/details/permission-details.component.spec.ts index 0c77ae679b7..bd8825da5c7 100644 --- a/feature-libs/organization/administration/components/permission/details/permission-details.component.spec.ts +++ b/feature-libs/organization/administration/components/permission/details/permission-details.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; @@ -26,13 +27,11 @@ import { MessageTestingModule } from '../../shared/message/message.testing.modul import { MessageService } from '../../shared/message/services/message.service'; import { PermissionDetailsComponent } from './permission-details.component'; -import createSpy = jasmine.createSpy; - const mockCode = 'p1'; class MockPermissionItemService implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } diff --git a/feature-libs/organization/administration/components/permission/form/permission-form.component.spec.ts b/feature-libs/organization/administration/components/permission/form/permission-form.component.spec.ts index 821573d31f2..58ea3b98926 100644 --- a/feature-libs/organization/administration/components/permission/form/permission-form.component.spec.ts +++ b/feature-libs/organization/administration/components/permission/form/permission-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, @@ -33,8 +34,6 @@ import { FormTestingModule } from '../../shared/form/form.testing.module'; import { PermissionItemService } from '../services/permission-item.service'; import { PermissionFormComponent } from './permission-form.component'; -import createSpy = jasmine.createSpy; - const mockForm = new UntypedFormGroup({ code: new UntypedFormControl(), periodRange: new UntypedFormControl(), @@ -76,7 +75,7 @@ const mockPermissionTypes: OrderApprovalPermissionType[] = [ }, ]; class MockPermissionService { - getTypes = createSpy('getTypes').and.returnValue(of(mockPermissionTypes)); + getTypes = vi.fn().mockReturnValue(of(mockPermissionTypes)); } describe('PermissionFormComponent', () => { @@ -130,9 +129,9 @@ describe('PermissionFormComponent', () => { currencyService = TestBed.inject(CurrencyService); b2bUnitService = TestBed.inject(OrgUnitService); - spyOn(currencyService, 'getAll').and.callThrough(); - spyOn(b2bUnitService, 'getActiveUnitList').and.callThrough(); - spyOn(b2bUnitService, 'loadList').and.callThrough(); + vi.spyOn(currencyService, 'getAll'); + vi.spyOn(b2bUnitService, 'getActiveUnitList'); + vi.spyOn(b2bUnitService, 'loadList'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/permission/services/permission-list.service.spec.ts b/feature-libs/organization/administration/components/permission/services/permission-list.service.spec.ts index e2bd17b96c0..0431ec5b0ca 100644 --- a/feature-libs/organization/administration/components/permission/services/permission-list.service.spec.ts +++ b/feature-libs/organization/administration/components/permission/services/permission-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel } from '@spartacus/core'; @@ -72,7 +73,7 @@ describe('PermissionListService', () => { }); it('should get empty table with 10 rows', () => { - spyOn(permissionService, 'getList').and.returnValue(of(undefined)); + vi.spyOn(permissionService, 'getList').mockReturnValue(of(undefined)); let result: EntitiesModel; service.getData().subscribe((table) => (result = table)); expect(result.values.length).toBe(10); diff --git a/feature-libs/organization/administration/components/shared/card/card.component.spec.ts b/feature-libs/organization/administration/components/shared/card/card.component.spec.ts index 14c2304f23c..a3b86c92747 100644 --- a/feature-libs/organization/administration/components/shared/card/card.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/card/card.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -24,14 +25,13 @@ import { ItemService } from '../item.service'; import { MessageComponent } from '../message/message.component'; import { MessageTestingModule } from '../message/message.testing.module'; import { CardComponent } from './card.component'; -import createSpy = jasmine.createSpy; const mockItem = { foo: 'bar' }; class MockItemService { key$ = of('key'); current$ = of(mockItem); - launchDetails = createSpy('launchDetails'); + launchDetails = vi.fn(); } class MockGlobalMessageService { @@ -114,14 +114,16 @@ describe('CardComponent', () => { const el: HTMLElement = fixture.debugElement.query( By.css('.title h3') ).nativeElement; - expect(el.innerText).toContain('organization.budget.title'); + expect(el.textContent?.trim()).toContain('organization.budget.title'); }); it('should have localized h4 subtitle', () => { const el: HTMLElement = fixture.debugElement.query( By.css('.title h4') ).nativeElement; - expect(el.innerText).toContain('organization.budget.subtitle'); + expect(el.textContent?.trim()).toContain( + 'organization.budget.subtitle' + ); }); it('should have back button by default', () => { @@ -146,7 +148,7 @@ describe('CardComponent', () => { const el: HTMLElement = fixture.debugElement.query( By.css('button.close') ).nativeElement; - expect(el.innerText).toContain('organization.assign'); + expect(el.textContent?.trim()).toContain('organization.assign'); }); }); }); @@ -157,7 +159,7 @@ describe('CardComponent', () => { const ev = { stopPropagation: () => {}, }; - spyOn(component.view, 'toggle'); + vi.spyOn(component.view, 'toggle'); component.closeView(ev as MouseEvent); expect(component.view.toggle).toHaveBeenCalledWith(true); }); @@ -183,7 +185,7 @@ describe('CardComponent', () => { By.css('cx-popover > .popover-body > p') ); expect(el).toBeTruthy(); - expect(el.nativeElement.innerText.trim()).toBe( + expect(el.nativeElement.textContent?.trim()).toBe( 'organization.budget.hint' ); }); diff --git a/feature-libs/organization/administration/components/shared/current-item.service.spec.ts b/feature-libs/organization/administration/components/shared/current-item.service.spec.ts index 7d074b32aa9..37b88502ec0 100644 --- a/feature-libs/organization/administration/components/shared/current-item.service.spec.ts +++ b/feature-libs/organization/administration/components/shared/current-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { RoutingService } from '@spartacus/core'; @@ -146,7 +147,7 @@ describe('CurrentItemService', () => { describe('model$', () => { it('should call getModel() with route parameter', () => { const mockBudget = { name: 'test cost center' }; - spyOn(service, 'getItem').and.returnValue(of(mockBudget)); + vi.spyOn(service, 'getItem').mockReturnValue(of(mockBudget)); let result; service.item$.subscribe((value) => (result = value)); @@ -156,7 +157,7 @@ describe('CurrentItemService', () => { }); it('should not call getModel() with route parameter', () => { - spyOn(service, 'getItem'); + vi.spyOn(service, 'getItem'); let result; service.item$.subscribe((value) => (result = value)); @@ -166,7 +167,7 @@ describe('CurrentItemService', () => { }); it('should resolve model', () => { - spyOn(service, 'getItem').and.returnValue( + vi.spyOn(service, 'getItem').mockReturnValue( of({ code: mockCode, name: 'I am a mock', @@ -181,7 +182,7 @@ describe('CurrentItemService', () => { }); it('should no longer resolve model', () => { - spyOn(service, 'getItem').and.returnValue( + vi.spyOn(service, 'getItem').mockReturnValue( of({ code: mockCode, name: 'I am a mock', diff --git a/feature-libs/organization/administration/components/shared/detail/delete-item-action/delete-item.component.spec.ts b/feature-libs/organization/administration/components/shared/detail/delete-item-action/delete-item.component.spec.ts index 109d5766f38..03a686ce5f4 100644 --- a/feature-libs/organization/administration/components/shared/detail/delete-item-action/delete-item.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/detail/delete-item-action/delete-item.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; @@ -11,7 +12,6 @@ import { ItemService } from '../../item.service'; import { ConfirmationMessageData } from '../../message/confirmation/confirmation-message.model'; import { MessageService } from '../../message/services/message.service'; import { DeleteItemComponent } from './delete-item.component'; -import createSpy = jasmine.createSpy; class MockMessageService { add() { @@ -68,7 +68,7 @@ describe('DeleteItemComponent', () => { organizationItemService = TestBed.inject(ItemService); messageService = TestBed.inject(MessageService); - spyOn(organizationItemService, 'delete').and.returnValue(EMPTY); + vi.spyOn(organizationItemService, 'delete').mockReturnValue(EMPTY); }); it('should not enable active items right away', () => { @@ -78,7 +78,7 @@ describe('DeleteItemComponent', () => { }); it('should prompt a disable confirmation prompt', () => { - spyOn(messageService, 'add').and.returnValue(new Subject()); + vi.spyOn(messageService, 'add').mockReturnValue(new Subject()); const mockItem = { code: 'b2', active: true }; component.delete(mockItem); expect(messageService.add).toHaveBeenCalledWith({ @@ -97,7 +97,7 @@ describe('DeleteItemComponent', () => { it('should confirm disabling', () => { const eventData: Subject = new Subject(); - spyOn(messageService, 'add').and.returnValue(eventData); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); const mockItem = { code: 'b2', active: true }; component.delete(mockItem); eventData.next({ confirm: true }); @@ -109,7 +109,7 @@ describe('DeleteItemComponent', () => { it('should confirm disabling with additional param', () => { const eventData: Subject = new Subject(); - spyOn(messageService, 'add').and.returnValue(eventData); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); const mockItem = { code: 'b2', active: true }; component.additionalParam = 'unitId'; component.delete(mockItem); @@ -124,10 +124,10 @@ describe('DeleteItemComponent', () => { const eventData: Subject = new Subject(); const mockItem = { code: 'b2', active: true }; const deletedItem = { code: 'b1', active: false }; - spyOn(messageService, 'add').and.returnValue(eventData); - organizationItemService.delete = createSpy().and.returnValue( - of({ status: LoadStatus.SUCCESS, item: deletedItem }) - ); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); + organizationItemService.delete = vi + .fn() + .mockReturnValue(of({ status: LoadStatus.SUCCESS, item: deletedItem })); component.delete(mockItem); eventData.next({ confirm: true }); expect(messageService.add).toHaveBeenCalledWith({ @@ -140,7 +140,7 @@ describe('DeleteItemComponent', () => { it('should cancel disabling', () => { const eventData: Subject = new Subject(); - spyOn(messageService, 'add').and.returnValue(eventData); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); const mockItem = { code: 'b2', active: true }; component.delete(mockItem); eventData.next({ close: true }); diff --git a/feature-libs/organization/administration/components/shared/detail/disable-info/disable-info.component.spec.ts b/feature-libs/organization/administration/components/shared/detail/disable-info/disable-info.component.spec.ts index 21773b17860..f1b83e36c74 100644 --- a/feature-libs/organization/administration/components/shared/detail/disable-info/disable-info.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/detail/disable-info/disable-info.component.spec.ts @@ -214,7 +214,7 @@ describe('ExplainDisableInfoComponent', () => { fixture.detectChanges(); const values = fixture.debugElement .queryAll(By.css('section > ul > li')) - .map((el) => el.nativeNode.innerText); + .map((el) => el.nativeNode.textContent?.trim()); expect(values).toEqual(expectedValue); } diff --git a/feature-libs/organization/administration/components/shared/detail/toggle-status-action/toggle-status.component.spec.ts b/feature-libs/organization/administration/components/shared/detail/toggle-status-action/toggle-status.component.spec.ts index 22e2d6cb446..3fede126961 100644 --- a/feature-libs/organization/administration/components/shared/detail/toggle-status-action/toggle-status.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/detail/toggle-status-action/toggle-status.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; @@ -11,7 +12,6 @@ import { ItemService } from '../../item.service'; import { ConfirmationMessageData } from '../../message/confirmation/confirmation-message.model'; import { MessageService } from '../../message/services/message.service'; import { ToggleStatusComponent } from './toggle-status.component'; -import createSpy = jasmine.createSpy; class MockMessageService { add() { @@ -94,7 +94,7 @@ describe('ToggleStatusComponent', () => { }); it('should enable inactive items right away', () => { - spyOn(organizationItemService, 'update').and.returnValue(EMPTY); + vi.spyOn(organizationItemService, 'update').mockReturnValue(EMPTY); const mockItem = { code: 'b1', active: false }; component.toggle(mockItem); expect(organizationItemService.update).toHaveBeenCalledWith( @@ -107,7 +107,7 @@ describe('ToggleStatusComponent', () => { }); it('should only patch code and active flag', () => { - spyOn(organizationItemService, 'update').and.returnValue(EMPTY); + vi.spyOn(organizationItemService, 'update').mockReturnValue(EMPTY); const mockItem = { code: 'b1', active: false, foo: 'bar' }; component.toggle(mockItem); expect(organizationItemService.update).toHaveBeenCalledWith( @@ -122,8 +122,8 @@ describe('ToggleStatusComponent', () => { it('should display confirmation for enabled item', () => { const mockItem = { code: 'b1', active: false }; const updatedItem = { code: 'b1', active: true }; - spyOn(messageService, 'add').and.returnValue(new Subject()); - spyOn(organizationItemService, 'update').and.returnValue( + vi.spyOn(messageService, 'add').mockReturnValue(new Subject()); + vi.spyOn(organizationItemService, 'update').mockReturnValue( of({ status: LoadStatus.SUCCESS, item: updatedItem }) ); component.toggle(mockItem); @@ -141,7 +141,7 @@ describe('ToggleStatusComponent', () => { organizationItemService = TestBed.inject(ItemService); messageService = TestBed.inject(MessageService); - spyOn(organizationItemService, 'update').and.returnValue(EMPTY); + vi.spyOn(organizationItemService, 'update').mockReturnValue(EMPTY); }); it('should not enable active items right away', () => { @@ -151,7 +151,7 @@ describe('ToggleStatusComponent', () => { }); it('should prompt a disable confirmation prompt', () => { - spyOn(messageService, 'add').and.returnValue(new Subject()); + vi.spyOn(messageService, 'add').mockReturnValue(new Subject()); const mockItem = { code: 'b2', active: true }; component.toggle(mockItem); expect(messageService.add).toHaveBeenCalledWith({ @@ -173,7 +173,7 @@ describe('ToggleStatusComponent', () => { it('should confirm disabling', () => { const eventData: Subject = new Subject(); - spyOn(messageService, 'add').and.returnValue(eventData); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); const mockItem = { code: 'b2', active: true }; component.toggle(mockItem); eventData.next({ confirm: true }); @@ -190,10 +190,10 @@ describe('ToggleStatusComponent', () => { const eventData: Subject = new Subject(); const mockItem = { code: 'b2', active: true }; const updatedItem = { code: 'b1', active: false }; - spyOn(messageService, 'add').and.returnValue(eventData); - organizationItemService.update = createSpy().and.returnValue( - of({ status: LoadStatus.SUCCESS, item: updatedItem }) - ); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); + organizationItemService.update = vi + .fn() + .mockReturnValue(of({ status: LoadStatus.SUCCESS, item: updatedItem })); component.toggle(mockItem); eventData.next({ confirm: true }); expect(messageService.add).toHaveBeenCalledWith({ @@ -206,7 +206,7 @@ describe('ToggleStatusComponent', () => { it('should cancel disabling', () => { const eventData: Subject = new Subject(); - spyOn(messageService, 'add').and.returnValue(eventData); + vi.spyOn(messageService, 'add').mockReturnValue(eventData); const mockItem = { code: 'b2', active: true }; component.toggle(mockItem); eventData.next({ close: true }); diff --git a/feature-libs/organization/administration/components/shared/form/form.component.spec.ts b/feature-libs/organization/administration/components/shared/form/form.component.spec.ts index 0f8b299c204..10fc4dbc232 100644 --- a/feature-libs/organization/administration/components/shared/form/form.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/form/form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; @@ -72,9 +73,9 @@ describe('FormComponent', () => { it('should save an updated item and notify', () => { const form = new UntypedFormGroup({}); - spyOn(organizationItemService, 'save').and.callThrough(); - spyOn(messageService, 'add').and.callThrough(); - spyOn(organizationItemService, 'launchDetails').and.callThrough(); + vi.spyOn(organizationItemService, 'save'); + vi.spyOn(messageService, 'add'); + vi.spyOn(organizationItemService, 'launchDetails'); key$.next('key'); component.save(form); @@ -93,9 +94,9 @@ describe('FormComponent', () => { it('should save an created item and notify', () => { const form = new UntypedFormGroup({}); - spyOn(organizationItemService, 'save').and.callThrough(); - spyOn(messageService, 'add').and.callThrough(); - spyOn(organizationItemService, 'launchDetails').and.callThrough(); + vi.spyOn(organizationItemService, 'save'); + vi.spyOn(messageService, 'add'); + vi.spyOn(organizationItemService, 'launchDetails'); key$.next(undefined); component.save(form); @@ -114,11 +115,11 @@ describe('FormComponent', () => { describe('when loading of the created item has failed', () => { beforeEach(() => { - spyOn(organizationItemService, 'save').and.returnValue( + vi.spyOn(organizationItemService, 'save').mockReturnValue( of({ status: LoadStatus.ERROR, item: mockItem }) ); - spyOn(messageService, 'add').and.callThrough(); - spyOn(organizationItemService, 'launchDetails').and.callThrough(); + vi.spyOn(messageService, 'add'); + vi.spyOn(organizationItemService, 'launchDetails'); }); it('should not launch details for not created item', () => { @@ -140,11 +141,11 @@ describe('FormComponent', () => { describe('when loading of the updated item has failed', () => { beforeEach(() => { - spyOn(organizationItemService, 'save').and.returnValue( + vi.spyOn(organizationItemService, 'save').mockReturnValue( of({ status: LoadStatus.ERROR, item: mockItem }) ); - spyOn(messageService, 'add').and.callThrough(); - spyOn(organizationItemService, 'launchDetails').and.callThrough(); + vi.spyOn(messageService, 'add'); + vi.spyOn(organizationItemService, 'launchDetails'); }); it('should not launch details for not updated item', () => { diff --git a/feature-libs/organization/administration/components/shared/form/form.testing.module.ts b/feature-libs/organization/administration/components/shared/form/form.testing.module.ts index 0432fff0db7..2b071e0594e 100644 --- a/feature-libs/organization/administration/components/shared/form/form.testing.module.ts +++ b/feature-libs/organization/administration/components/shared/form/form.testing.module.ts @@ -7,7 +7,6 @@ import { Component, Input, NgModule } from '@angular/core'; import { CurrentItemService } from '../current-item.service'; import { FormService } from './form.service'; -import createSpy = jasmine.createSpy; @Component({ selector: 'cx-org-form', @@ -21,7 +20,7 @@ export class MockBudgetFormService {} class MockCurrentItemService {} class MockFormService { - getForm = createSpy('getForm'); + getForm = () => {}; } @NgModule({ diff --git a/feature-libs/organization/administration/components/shared/item-active.directive.spec.ts b/feature-libs/organization/administration/components/shared/item-active.directive.spec.ts index ef44fe7fb35..29d608baca7 100644 --- a/feature-libs/organization/administration/components/shared/item-active.directive.spec.ts +++ b/feature-libs/organization/administration/components/shared/item-active.directive.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GlobalMessageType } from '@spartacus/core'; @@ -6,8 +7,6 @@ import { ItemActiveDirective } from './item-active.directive'; import { ItemService } from './item.service'; import { MessageService } from './message/services/message.service'; -import createSpy = jasmine.createSpy; - const mockCode = 'mc1'; @Component({ @@ -19,7 +18,7 @@ const mockCode = 'mc1'; class TestComponent {} class MockMessageService { - add = createSpy('add').and.returnValue(new Subject()); + add = vi.fn().mockReturnValue(new Subject()); clear() {} close() {} } @@ -34,14 +33,14 @@ const itemStubInactive = { class MockItemServiceActive implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); current$ = of(itemStubActive); } class MockItemServiceInactive implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); current$ = of(itemStubInactive); } diff --git a/feature-libs/organization/administration/components/shared/item-exists.directive.spec.ts b/feature-libs/organization/administration/components/shared/item-exists.directive.spec.ts index de85afb0e0d..2ad15c13bd2 100644 --- a/feature-libs/organization/administration/components/shared/item-exists.directive.spec.ts +++ b/feature-libs/organization/administration/components/shared/item-exists.directive.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UntypedFormGroup } from '@angular/forms'; @@ -6,7 +7,6 @@ import { EMPTY, of, Subject } from 'rxjs'; import { ItemExistsDirective } from './item-exists.directive'; import { ItemService } from './item.service'; import { MessageService } from './message/services/message.service'; -import createSpy = jasmine.createSpy; const mockCode = 'mc1'; @@ -21,20 +21,20 @@ class TestComponent { } class MockMessageService { - add = createSpy('add').and.returnValue(new Subject()); + add = vi.fn().mockReturnValue(new Subject()); clear() {} close() {} } class MockItemServiceWithError implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(true); } class MockItemServiceWithoutError implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } diff --git a/feature-libs/organization/administration/components/shared/item.service.spec.ts b/feature-libs/organization/administration/components/shared/item.service.spec.ts index 79d207cf78f..0d88ed3afaf 100644 --- a/feature-libs/organization/administration/components/shared/item.service.spec.ts +++ b/feature-libs/organization/administration/components/shared/item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { @@ -10,11 +11,10 @@ import { LoadStatus, OrganizationItemStatus, } from '@spartacus/organization/administration/core'; -import { EMPTY, Observable, of } from 'rxjs'; +import { EMPTY, firstValueFrom, Observable, of } from 'rxjs'; import { CurrentItemService } from './current-item.service'; import { FormService } from './form/form.service'; import { ItemService } from './item.service'; -import createSpy = jasmine.createSpy; const mockCode = 'o1'; class MockRoutingService { @@ -23,7 +23,7 @@ class MockRoutingService { class MockCurrentItemService { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } @@ -77,7 +77,7 @@ describe('ItemService', () => { formService = TestBed.inject(FormService); routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); }); it('should be created', () => { @@ -85,7 +85,7 @@ describe('ItemService', () => { }); it('should return form', () => { - spyOn(formService, 'getForm').and.callThrough(); + vi.spyOn(formService, 'getForm'); expect(service.getForm()).toEqual(mockForm); }); @@ -102,7 +102,7 @@ describe('ItemService', () => { describe('save()', () => { describe('handle valid form data', () => { it('should create new item', () => { - spyOn(service, 'create').and.callThrough(); + vi.spyOn(service, 'create'); const form = new UntypedFormGroup({}); form.addControl('name', new UntypedFormControl('foo bar')); expect(service.save(form)).toEqual(mockItemStatus); @@ -113,7 +113,7 @@ describe('ItemService', () => { }); it('should update existing item', () => { - spyOn(service, 'update').and.callThrough(); + vi.spyOn(service, 'update'); const form = new UntypedFormGroup({}); form.addControl('name', new UntypedFormControl('foo bar')); @@ -127,7 +127,7 @@ describe('ItemService', () => { describe('handle invalid form data', () => { it('should not create invalid existing item', () => { - spyOn(service, 'create').and.callThrough(); + vi.spyOn(service, 'create'); const form = new UntypedFormGroup({}); form.addControl( undefined, @@ -140,7 +140,7 @@ describe('ItemService', () => { }); it('should not update invalid existing item', () => { - spyOn(service, 'update').and.callThrough(); + vi.spyOn(service, 'update'); const form = new UntypedFormGroup({}); form.addControl( 'name', @@ -154,29 +154,21 @@ describe('ItemService', () => { }); describe('isInEditMode', () => { - it('should emit false after component creation', (done) => { - service.isInEditMode$.subscribe((result) => { - expect(result).toBe(false); - done(); - }); + it('should emit false after component creation', async () => { + const result = await firstValueFrom(service.isInEditMode$); + expect(result).toBe(false); }); - it('when set to true should emit true', (done) => { + it('when set to true should emit true', async () => { service.setEditMode(true); - - service.isInEditMode$.subscribe((result) => { - expect(result).toBe(true); - done(); - }); + const result = await firstValueFrom(service.isInEditMode$); + expect(result).toBe(true); }); - it('when set to false should emit false', (done) => { + it('when set to false should emit false', async () => { service.setEditMode(false); - - service.isInEditMode$.subscribe((result) => { - expect(result).toBe(false); - done(); - }); + const result = await firstValueFrom(service.isInEditMode$); + expect(result).toBe(false); }); }); }); diff --git a/feature-libs/organization/administration/components/shared/list/list.component.spec.ts b/feature-libs/organization/administration/components/shared/list/list.component.spec.ts index 1b79c467231..108c76f3203 100644 --- a/feature-libs/organization/administration/components/shared/list/list.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/list/list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Component, @@ -15,7 +16,6 @@ import { EntitiesModel, FeatureDirective, I18nTestingModule, - Translatable, TranslatePipe, UrlPipe, } from '@spartacus/core'; @@ -39,7 +39,6 @@ import { EMPTY, of } from 'rxjs'; import { ItemService } from '../item.service'; import { ListComponent } from './list.component'; import { ListService } from './list.service'; -import createSpy = jasmine.createSpy; interface Mock { code: string; @@ -69,10 +68,10 @@ const mockEmptyList: EntitiesModel = { }; class MockBaseListService { - view = createSpy('view'); - sort = createSpy('sort'); - search = createSpy('search'); - clearSearch = createSpy('clearSearch'); + view = vi.fn(); + sort = vi.fn(); + search = vi.fn(); + clearSearch = vi.fn(); getData() { return EMPTY; } @@ -95,15 +94,13 @@ class MockBaseListService { return 'organization.search.placeholder'; } onCreateButtonClick(): void {} - getCreateButtonType = createSpy('getCreateButtonType'); - getCreateButtonLabel(): Translatable { - return { key: 'organization.add' }; - } + getCreateButtonType = vi.fn(); + getCreateButtonLabel = vi.fn().mockReturnValue({ key: 'organization.add' }); } class MockItemService { key$ = EMPTY; - launchDetails = createSpy('launchDetails'); + launchDetails = vi.fn(); } class ActivatedRouteMock { @@ -209,8 +206,8 @@ describe('ListComponent', () => { describe('with table data', () => { beforeEach(() => { - spyOn(service, 'getData').and.returnValue(of(mockList)); - spyOn(service, 'key').and.callThrough(); + vi.spyOn(service, 'getData').mockReturnValue(of(mockList)); + vi.spyOn(service, 'key'); fixture = TestBed.createComponent(MockListComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -273,7 +270,7 @@ describe('ListComponent', () => { describe('without table data', () => { beforeEach(() => { - spyOn(service, 'getData').and.returnValue(of(mockEmptyList)); + vi.spyOn(service, 'getData').mockReturnValue(of(mockEmptyList)); fixture = TestBed.createComponent(MockListComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -292,7 +289,7 @@ describe('ListComponent', () => { describe('hint', () => { beforeEach(() => { - spyOn(service, 'getData').and.returnValue(of(mockEmptyList)); + vi.spyOn(service, 'getData').mockReturnValue(of(mockEmptyList)); fixture = TestBed.createComponent(MockListComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -312,19 +309,19 @@ describe('ListComponent', () => { By.css('cx-popover > .popover-body > p') ); expect(el).toBeTruthy(); - expect(el.nativeElement.innerText.trim()).toBe('orgBudget.hint'); + expect(el.nativeElement.textContent?.trim()).toBe('orgBudget.hint'); }); }); describe('onCreateButtonClick', () => { beforeEach(() => { - spyOn(service, 'getData').and.returnValue(of(mockEmptyList)); + vi.spyOn(service, 'getData').mockReturnValue(of(mockEmptyList)); fixture = TestBed.createComponent(MockListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should process click of create button', () => { - spyOn(service, 'onCreateButtonClick').and.callThrough(); + vi.spyOn(service, 'onCreateButtonClick'); component.onCreateButtonClick(); expect(service.onCreateButtonClick).toHaveBeenCalled(); }); @@ -334,17 +331,16 @@ describe('ListComponent', () => { let el: DebugElement; beforeEach(() => { - spyOn(service, 'getData').and.returnValue(of(mockEmptyList)); + vi.spyOn(service, 'getData').mockReturnValue(of(mockEmptyList)); fixture = TestBed.createComponent(MockListComponent); el = fixture.debugElement; component = fixture.componentInstance; - fixture.detectChanges(); }); describe('it should show create functionality by default', () => { it('it should show Hyperlink with correct label and not Button', () => { - service.getCreateButtonType = createSpy().and.returnValue('LINK'); - service.getCreateButtonLabel = createSpy().and.returnValue({ + service.getCreateButtonType = vi.fn().mockReturnValue('LINK'); + service.getCreateButtonLabel = vi.fn().mockReturnValue({ key: 'organization.add', }); component.createButtonType = service.getCreateButtonType(); @@ -353,14 +349,16 @@ describe('ListComponent', () => { let hlink = el.query(By.css('a.button.primary.create')); expect(hlink).toBeTruthy(); - expect(hlink.nativeElement.innerText).toBe('organization.add'); + expect(hlink.nativeElement.textContent?.trim()).toBe( + 'organization.add' + ); let button = el.query(By.css('button.button.primary.create')); expect(button).toBeNull(); }); it('it should show Button with correct label and not Hyperlink', () => { - service.getCreateButtonType = createSpy().and.returnValue('BUTTON'); - service.getCreateButtonLabel = createSpy().and.returnValue({ + service.getCreateButtonType = vi.fn().mockReturnValue('BUTTON'); + service.getCreateButtonLabel = vi.fn().mockReturnValue({ key: 'organization.manageUsers', }); component.createButtonType = service.getCreateButtonType(); @@ -371,13 +369,15 @@ describe('ListComponent', () => { expect(hlink).toBeNull(); let button = el.query(By.css('button.button.primary.create')); expect(button).toBeTruthy(); - expect(button.nativeElement.innerText).toBe('organization.manageUsers'); + expect(button.nativeElement.textContent?.trim()).toBe( + 'organization.manageUsers' + ); }); }); describe('it should not show create functionality', () => { it('it should not show Hyperlink', () => { - service.getCreateButtonType = createSpy().and.returnValue('LINK'); + service.getCreateButtonType = vi.fn().mockReturnValue('LINK'); component.hideAddButton = true; component.createButtonType = service.getCreateButtonType(); fixture.detectChanges(); @@ -389,7 +389,7 @@ describe('ListComponent', () => { }); it('it should not show Button', () => { - service.getCreateButtonType = createSpy().and.returnValue('BUTTON'); + service.getCreateButtonType = vi.fn().mockReturnValue('BUTTON'); component.createButtonType = service.getCreateButtonType(); component.hideAddButton = true; fixture.detectChanges(); @@ -404,7 +404,7 @@ describe('ListComponent', () => { describe('Search functionality', () => { beforeEach(() => { - spyOn(service, 'getData').and.returnValue(of(mockList)); + vi.spyOn(service, 'getData').mockReturnValue(of(mockList)); fixture = TestBed.createComponent(MockListComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -417,7 +417,7 @@ describe('ListComponent', () => { }); it('should reflect service isSearchEnabled value', () => { - spyOn(service, 'isSearchEnabled').and.returnValue(true); + vi.spyOn(service, 'isSearchEnabled').mockReturnValue(true); const newFixture = TestBed.createComponent(MockListComponent); const newComponent = newFixture.componentInstance; expect(newComponent.isSearchEnabled).toBe(true); diff --git a/feature-libs/organization/administration/components/shared/list/list.service.spec.ts b/feature-libs/organization/administration/components/shared/list/list.service.spec.ts index 93b4210a466..30bae5a0d0a 100644 --- a/feature-libs/organization/administration/components/shared/list/list.service.spec.ts +++ b/feature-libs/organization/administration/components/shared/list/list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel, PaginationModel } from '@spartacus/core'; @@ -68,7 +69,7 @@ describe('ListService', () => { describe('getData', () => { it('should call load method to get data', () => { - spyOn(service, 'load').and.callThrough(); + vi.spyOn(service, 'load'); service.getData().subscribe(); expect(service.load).toHaveBeenCalled(); }); @@ -90,7 +91,7 @@ describe('ListService', () => { it('should use pageSize=3 from configurable structure', () => { let result: EntitiesModel; - spyOn(service, 'getStructure').and.returnValue( + vi.spyOn(service, 'getStructure').mockReturnValue( of({ options: { pagination: { pageSize: 3 } } } as TableStructure) ); service @@ -130,7 +131,7 @@ describe('ListService', () => { describe('getStructure()', () => { it('should build structure with tableService', () => { - spyOn(tableService, 'buildStructure').and.returnValue( + vi.spyOn(tableService, 'buildStructure').mockReturnValue( of({ options: { pagination: { pageSize: 3 } } } as TableStructure) ); service.getStructure().subscribe().unsubscribe(); diff --git a/feature-libs/organization/administration/components/shared/message/base-message.component.spec.ts b/feature-libs/organization/administration/components/shared/message/base-message.component.spec.ts index cf44975a500..d7379efb647 100644 --- a/feature-libs/organization/administration/components/shared/message/base-message.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/message/base-message.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -110,14 +111,17 @@ describe('BaseMessageComponent', () => { }); describe('close()', () => { - beforeEach(function () { - // https://github.com/gruntjs/grunt-contrib-jasmine/issues/213 - jasmine.clock().uninstall(); - jasmine.clock().install(); + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); }); it('should emit close event', () => { - const nextEvent = spyOn(messageData.events, 'next'); + const nextEvent = vi.spyOn(messageData.events, 'next'); component.close(); expect(nextEvent).toHaveBeenCalledWith({ close: true, @@ -125,12 +129,12 @@ describe('BaseMessageComponent', () => { }); it('should close after message timeout', () => { - const nextEvent = spyOn(messageData.events, 'next'); + const nextEvent = vi.spyOn(messageData.events, 'next'); messageData.timeout = 10; component.ngOnInit(); expect(nextEvent).not.toHaveBeenCalled(); - jasmine.clock().tick(10); + vi.advanceTimersByTime(10); expect(nextEvent).toHaveBeenCalledWith({ close: true, }); diff --git a/feature-libs/organization/administration/components/shared/message/confirmation/confirmation-message.component.spec.ts b/feature-libs/organization/administration/components/shared/message/confirmation/confirmation-message.component.spec.ts index c9bc2d1ed41..dad6921c888 100644 --- a/feature-libs/organization/administration/components/shared/message/confirmation/confirmation-message.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/message/confirmation/confirmation-message.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -54,7 +55,11 @@ describe('ConfirmationMessageComponent', () => { fixture.detectChanges(); }); - it('should create component', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should create', () => { expect(component).toBeTruthy(); }); @@ -62,7 +67,7 @@ describe('ConfirmationMessageComponent', () => { const messageEl: HTMLElement = fixture.debugElement.query( By.css('.message p') ).nativeElement; - expect(messageEl.innerText).toEqual('Raw mock message'); + expect(messageEl.textContent?.trim()).toEqual('Raw mock message'); }); it('should have confirm button', () => { @@ -80,7 +85,7 @@ describe('ConfirmationMessageComponent', () => { }); it('should emit confirm event', () => { - const nextEvent = spyOn(messageData.events, 'next'); + const nextEvent = vi.spyOn(messageData.events, 'next'); const el: HTMLElement = fixture.debugElement.query( By.css('button.confirm') ).nativeElement; @@ -93,7 +98,7 @@ describe('ConfirmationMessageComponent', () => { }); it('should not emit confirm event', () => { - const nextEvent = spyOn(messageData.events, 'next'); + const nextEvent = vi.spyOn(messageData.events, 'next'); const el: HTMLElement = fixture.debugElement.query( By.css('button.cancel') ).nativeElement; diff --git a/feature-libs/organization/administration/components/shared/message/message.component.spec.ts b/feature-libs/organization/administration/components/shared/message/message.component.spec.ts index 2ffe8b5ac2a..d71458d873f 100644 --- a/feature-libs/organization/administration/components/shared/message/message.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/message/message.component.spec.ts @@ -81,11 +81,11 @@ describe('MessageComponent', () => { const lastMessage: HTMLElement = fixture.debugElement.query( By.css('cx-org-notification:first-child') ).nativeElement; - expect(lastMessage.innerText).toEqual('mock message 2'); + expect(lastMessage.textContent?.trim()).toEqual('mock message 2'); const firstMessage: HTMLElement = fixture.debugElement.query( By.css('cx-org-notification:last-child') ).nativeElement; - expect(firstMessage.innerText).toEqual('mock message 1'); + expect(firstMessage.textContent?.trim()).toEqual('mock message 1'); }); }); diff --git a/feature-libs/organization/administration/components/shared/message/notification/notification-message.component.spec.ts b/feature-libs/organization/administration/components/shared/message/notification/notification-message.component.spec.ts index c5405d78964..228d2739616 100644 --- a/feature-libs/organization/administration/components/shared/message/notification/notification-message.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/message/notification/notification-message.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -61,7 +62,7 @@ describe('NotificationMessageComponent', () => { const el: HTMLElement = fixture.debugElement.query( By.css('p') ).nativeElement; - expect(el.innerText).toEqual('Raw mock message'); + expect(el.textContent?.trim()).toEqual('Raw mock message'); }); it('should have close button', () => { @@ -72,7 +73,7 @@ describe('NotificationMessageComponent', () => { }); it('should emit close event', () => { - const nextEvent = spyOn(messageData.events, 'next'); + const nextEvent = vi.spyOn(messageData.events, 'next'); const el: HTMLElement = fixture.debugElement.query( By.css('button.close') ).nativeElement; diff --git a/feature-libs/organization/administration/components/shared/sub-list/assign-cell.component.spec.ts b/feature-libs/organization/administration/components/shared/sub-list/assign-cell.component.spec.ts index d294223a2e5..8fc35020f75 100644 --- a/feature-libs/organization/administration/components/shared/sub-list/assign-cell.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/sub-list/assign-cell.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { @@ -105,11 +106,8 @@ describe('AssignCellComponent', () => { }); it('should unassign', () => { - spyOn( - organizationListService as SubListService, - 'unassign' - ).and.callThrough(); - spyOn(messageService, 'add').and.callThrough(); + vi.spyOn(organizationListService as SubListService, 'unassign'); + vi.spyOn(messageService, 'add'); component.toggleAssign(); @@ -152,11 +150,8 @@ describe('AssignCellComponent', () => { }); it('should assign', () => { - spyOn( - organizationListService as SubListService, - 'assign' - ).and.callThrough(); - spyOn(messageService, 'add').and.callThrough(); + vi.spyOn(organizationListService as SubListService, 'assign'); + vi.spyOn(messageService, 'add'); component.toggleAssign(); diff --git a/feature-libs/organization/administration/components/shared/sub-list/sub-list.component.spec.ts b/feature-libs/organization/administration/components/shared/sub-list/sub-list.component.spec.ts index 04b2ab12ad4..3df34f56ddb 100644 --- a/feature-libs/organization/administration/components/shared/sub-list/sub-list.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/sub-list/sub-list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Component, @@ -32,7 +33,6 @@ import { ListService } from '../list/list.service'; import { MessageComponent } from '../message/message.component'; import { MessageTestingModule } from '../message/message.testing.module'; import { SubListComponent } from './sub-list.component'; -import createSpy = jasmine.createSpy; const mockList: EntitiesModel = { values: [ @@ -71,8 +71,8 @@ class MockTableComponent { } class MockBaseListService { - view = createSpy('view'); - sort = createSpy('sort'); + view = vi.fn(); + sort = vi.fn(); getData() { return of(null); } @@ -95,12 +95,12 @@ class MockBaseListService { return 'organization.search.placeholder'; } onCreateButtonClick(): void {} - getCreateButtonType = createSpy('getCreateButtonType'); + getCreateButtonType = vi.fn(); } class MockItemService { key$ = of('key'); - launchDetails = createSpy('launchDetails'); + launchDetails = vi.fn(); } class ActivatedRouteMock { @@ -176,7 +176,9 @@ describe('SubListComponent', () => { describe('with data', () => { beforeEach(() => { - spyOn(organizationListService, 'getData').and.returnValue(of(mockList)); + vi.spyOn(organizationListService, 'getData').mockReturnValue( + of(mockList) + ); fixture = TestBed.createComponent(SubListComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -204,7 +206,7 @@ describe('SubListComponent', () => { describe('without data', () => { beforeEach(() => { - spyOn(organizationListService, 'getData').and.returnValue( + vi.spyOn(organizationListService, 'getData').mockReturnValue( of(mockEmptyList) ); fixture = TestBed.createComponent(SubListComponent); diff --git a/feature-libs/organization/administration/components/shared/table/cell.component.spec.ts b/feature-libs/organization/administration/components/shared/table/cell.component.spec.ts index a570719e9f7..366fabad816 100644 --- a/feature-libs/organization/administration/components/shared/table/cell.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/table/cell.component.spec.ts @@ -99,6 +99,6 @@ describe('CellComponent', () => { const el: HTMLElement = fixture.debugElement.query( By.css('span.text') ).nativeNode; - expect(el.innerText).toEqual('my name'); + expect(el.textContent?.trim()).toEqual('my name'); }); }); diff --git a/feature-libs/organization/administration/components/shared/table/date-range/date-range-cell.component.spec.ts b/feature-libs/organization/administration/components/shared/table/date-range/date-range-cell.component.spec.ts index 6a7983497c2..510df4bd15b 100644 --- a/feature-libs/organization/administration/components/shared/table/date-range/date-range-cell.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/table/date-range/date-range-cell.component.spec.ts @@ -55,6 +55,6 @@ describe('DateRangeCellComponent', () => { const el: HTMLElement = fixture.debugElement.query( By.css('span.text') ).nativeNode; - expect(el.innerText).toEqual('Jul 15, 2020 - Jul 15, 2020'); + expect(el.textContent?.trim()).toEqual('Jul 15, 2020 - Jul 15, 2020'); }); }); diff --git a/feature-libs/organization/administration/components/shared/table/limit/limit-cell.component.spec.ts b/feature-libs/organization/administration/components/shared/table/limit/limit-cell.component.spec.ts index e6732dc52ac..e0a70d5a8ad 100644 --- a/feature-libs/organization/administration/components/shared/table/limit/limit-cell.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/table/limit/limit-cell.component.spec.ts @@ -45,6 +45,8 @@ describe('LimitCellComponent', () => { const el: HTMLElement = fixture.debugElement.query( By.css('span.text') ).nativeNode; - expect(el.innerText).toEqual('10000 $ orgPurchaseLimit.per.QUARTER'); + expect(el.textContent?.trim()).toEqual( + '10000 $ orgPurchaseLimit.per.QUARTER' + ); }); }); diff --git a/feature-libs/organization/administration/components/shared/table/roles/roles-cell.component.spec.ts b/feature-libs/organization/administration/components/shared/table/roles/roles-cell.component.spec.ts index a06608c6bab..a54cea369ea 100644 --- a/feature-libs/organization/administration/components/shared/table/roles/roles-cell.component.spec.ts +++ b/feature-libs/organization/administration/components/shared/table/roles/roles-cell.component.spec.ts @@ -38,7 +38,6 @@ describe('RolesCellComponent', () => { it('should render roles', () => { const el = fixture.debugElement.queryAll(By.css('ul.text li')); expect(el.length).toEqual(2); - expect((el[0].nativeElement as HTMLElement).innerText).toEqual( 'organization.userRoles.approver' ); diff --git a/feature-libs/organization/administration/components/unit/form/unit-form.component.spec.ts b/feature-libs/organization/administration/components/unit/form/unit-form.component.spec.ts index dff25d9e336..d5145d05bca 100644 --- a/feature-libs/organization/administration/components/unit/form/unit-form.component.spec.ts +++ b/feature-libs/organization/administration/components/unit/form/unit-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, @@ -104,9 +105,9 @@ describe('UnitFormComponent', () => { b2bUnitService = TestBed.inject(OrgUnitService); - spyOn(b2bUnitService, 'getActiveUnitList').and.callThrough(); - spyOn(b2bUnitService, 'loadList').and.callThrough(); - spyOn(b2bUnitService, 'getApprovalProcesses').and.callThrough(); + vi.spyOn(b2bUnitService, 'getActiveUnitList'); + vi.spyOn(b2bUnitService, 'loadList'); + vi.spyOn(b2bUnitService, 'getApprovalProcesses'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/unit/links/approvers/assigned/unit-assigned-approver-list.service.spec.ts b/feature-libs/organization/administration/components/unit/links/approvers/assigned/unit-assigned-approver-list.service.spec.ts index 59b88c57930..5b22820fd7d 100644 --- a/feature-libs/organization/administration/components/unit/links/approvers/assigned/unit-assigned-approver-list.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/links/approvers/assigned/unit-assigned-approver-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { B2BUnit, B2BUser, B2BUserRole, EntitiesModel } from '@spartacus/core'; @@ -87,8 +88,8 @@ describe('UnitAssignedApproverListService', () => { }); it('should clear approvers data before load', () => { - spyOn(unitService, 'clearAssignedUsersList'); - spyOn(unitService, 'getUsers').and.returnValue(EMPTY); + vi.spyOn(unitService, 'clearAssignedUsersList'); + vi.spyOn(unitService, 'getUsers').mockReturnValue(EMPTY); service.getData('u1').subscribe(); expect(unitService.clearAssignedUsersList).toHaveBeenCalledWith( diff --git a/feature-libs/organization/administration/components/unit/links/approvers/unit-approver-list.service.spec.ts b/feature-libs/organization/administration/components/unit/links/approvers/unit-approver-list.service.spec.ts index cc370e58b0d..d977e7f4bbc 100644 --- a/feature-libs/organization/administration/components/unit/links/approvers/unit-approver-list.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/links/approvers/unit-approver-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { B2BUser, B2BUserRole, EntitiesModel } from '@spartacus/core'; @@ -100,7 +101,7 @@ describe('UnitApproverListService', () => { }); it('should load users with "b2bapprovergroup" role', () => { - spyOn(unitService, 'getUsers').and.returnValue(EMPTY); + vi.spyOn(unitService, 'getUsers').mockReturnValue(EMPTY); service.getData('u1').subscribe().unsubscribe(); @@ -114,8 +115,8 @@ describe('UnitApproverListService', () => { }); it('should assign approver', () => { - spyOn(unitService, 'assignApprover').and.callThrough(); - spyOn(userService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(unitService, 'assignApprover'); + vi.spyOn(userService, 'getLoadingStatus'); expect(service.assign(unitId, approverId)).toEqual(mockItemStatus); expect(unitService.assignApprover).toHaveBeenCalledWith( @@ -127,8 +128,8 @@ describe('UnitApproverListService', () => { }); it('should unassign approver', () => { - spyOn(unitService, 'unassignApprover').and.callThrough(); - spyOn(userService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(unitService, 'unassignApprover'); + vi.spyOn(userService, 'getLoadingStatus'); expect(service.unassign(unitId, approverId)).toEqual(mockItemStatus); expect(unitService.unassignApprover).toHaveBeenCalledWith( diff --git a/feature-libs/organization/administration/components/unit/links/children/create/current-child-unit.service.spec.ts b/feature-libs/organization/administration/components/unit/links/children/create/current-child-unit.service.spec.ts index 0b57b46259d..12278859a09 100644 --- a/feature-libs/organization/administration/components/unit/links/children/create/current-child-unit.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/links/children/create/current-child-unit.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { RoutingService } from '@spartacus/core'; import { OrgUnitService } from '@spartacus/organization/administration/core'; @@ -46,7 +47,7 @@ describe('CurrentUnitChildService', () => { describe('model$', () => { it('should not load unit for child units', () => { - spyOn(unitService, 'get').and.callThrough(); + vi.spyOn(unitService, 'get'); service.item$.subscribe(); mockParams.next({ [ROUTE_PARAMS.unitCode]: '123' }); expect(unitService.get).not.toHaveBeenCalled(); diff --git a/feature-libs/organization/administration/components/unit/links/children/create/unit-child-item.service.spec.ts b/feature-libs/organization/administration/components/unit/links/children/create/unit-child-item.service.spec.ts index 917c39a2636..5f77841d5d3 100644 --- a/feature-libs/organization/administration/components/unit/links/children/create/unit-child-item.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/links/children/create/unit-child-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RoutingService } from '@spartacus/core'; @@ -11,7 +12,6 @@ import { EMPTY, Observable, of } from 'rxjs'; import { UnitFormService } from '../../../form/unit-form.service'; import { CurrentUnitChildService } from './current-unit-child.service'; import { UnitChildItemService } from './unit-child-item.service'; -import createSpy = jasmine.createSpy; const mockCode = 'u1'; class MockRoutingService { @@ -33,7 +33,7 @@ class MockOrgUnitService { class MockUnitFormService {} class MockCurrentUnitChildService { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } describe('UnitChildItemService', () => { @@ -63,7 +63,7 @@ describe('UnitChildItemService', () => { }); it('should create item with parentUnitUid', () => { - spyOn(unitService, 'create').and.callThrough(); + vi.spyOn(unitService, 'create'); const form = new UntypedFormGroup({}); form.setControl('name', new UntypedFormControl('Child Unit Name')); form.setControl( @@ -83,7 +83,7 @@ describe('UnitChildItemService', () => { it('should launch orgUnitChildren with parentUnitUid uid', () => { const routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.launchDetails({ uid: 'child-uid', name: 'foo bar', diff --git a/feature-libs/organization/administration/components/unit/links/cost-centers/create/unit-cost-center-item.service.spec.ts b/feature-libs/organization/administration/components/unit/links/cost-centers/create/unit-cost-center-item.service.spec.ts index a7e5a9da930..a254e047d18 100644 --- a/feature-libs/organization/administration/components/unit/links/cost-centers/create/unit-cost-center-item.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/links/cost-centers/create/unit-cost-center-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RoutingService } from '@spartacus/core'; @@ -11,7 +12,6 @@ import { EMPTY, Observable, of } from 'rxjs'; import { CostCenterFormService } from '../../../../cost-center/form/cost-center-form.service'; import { CurrentCostCenterService } from '../../../../cost-center/services/current-cost-center.service'; import { UnitCostCenterItemService } from './unit-cost-center-item.service'; -import createSpy = jasmine.createSpy; const mockCode = 'c1'; class MockRoutingService { @@ -36,7 +36,7 @@ class MockCostCenterFormService {} class MockCurrentCostCenterService { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } describe('UnitCostCenterItemService', () => { @@ -66,7 +66,7 @@ describe('UnitCostCenterItemService', () => { }); it('should create cost center with unit.uid', () => { - spyOn(costCenterService, 'create').and.callThrough(); + vi.spyOn(costCenterService, 'create'); const form = new UntypedFormGroup({}); form.setControl('name', new UntypedFormControl('cc name')); form.setControl( @@ -86,7 +86,7 @@ describe('UnitCostCenterItemService', () => { it('should launch orgUnitCostCenters with unit uid', () => { const routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.launchDetails({ code: 'c-1', name: 'foo bar', diff --git a/feature-libs/organization/administration/components/unit/links/users/create/unit-user-item.service.spec.ts b/feature-libs/organization/administration/components/unit/links/users/create/unit-user-item.service.spec.ts index 7b1725d0b19..ce824ce8c77 100644 --- a/feature-libs/organization/administration/components/unit/links/users/create/unit-user-item.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/links/users/create/unit-user-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RoutingService } from '@spartacus/core'; @@ -57,7 +58,7 @@ describe('ChildUnitItemService', () => { }); it('should create item with unitUid', () => { - spyOn(userService, 'create').and.callThrough(); + vi.spyOn(userService, 'create'); const form = new UntypedFormGroup({}); form.setControl('name', new UntypedFormControl('User name')); form.setControl( @@ -78,7 +79,7 @@ describe('ChildUnitItemService', () => { it('should launch orgUnitChildren with unitUid uid', () => { const routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.launchDetails({ uid: 'uid', name: 'foo bar', diff --git a/feature-libs/organization/administration/components/unit/list/toggle-link/toggle-link-cell.component.spec.ts b/feature-libs/organization/administration/components/unit/list/toggle-link/toggle-link-cell.component.spec.ts index f9967d8bde1..328dff10209 100644 --- a/feature-libs/organization/administration/components/unit/list/toggle-link/toggle-link-cell.component.spec.ts +++ b/feature-libs/organization/administration/components/unit/list/toggle-link/toggle-link-cell.component.spec.ts @@ -1,4 +1,5 @@ -import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { @@ -17,7 +18,6 @@ import { MockUrlPipe } from 'core-libs/core/src/routing/configurable-routes/url- import { BehaviorSubject, of } from 'rxjs'; import { UnitTreeService } from '../../services/unit-tree.service'; import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/feature-toggles/testing'; -import createSpy = jasmine.createSpy; const mockContext = { expanded: true, @@ -31,7 +31,7 @@ const mockContext = { }; class MockUnitTreeService implements Partial { - toggle = createSpy('toggle'); + toggle = vi.fn(); treeToggle$ = new BehaviorSubject(new Map()); } @@ -99,7 +99,7 @@ describe('ToggleLinkCellComponent', () => { it('should render tabindex = 0 by default', () => { const el: HTMLElement = fixture.debugElement.query(By.css('a')).nativeNode; - expect(el.innerText).toEqual('my name (1)'); + expect(el.textContent?.trim()).toEqual('my name (1)'); expect(el.tabIndex).toEqual(0); }); @@ -129,22 +129,18 @@ describe('ToggleLinkCellComponent', () => { it('should enable keyboard controls', () => { const mockTableElement = { - querySelectorAll: jasmine - .createSpy('querySelectorAll') - .and.returnValue(mockSiblingElements), + querySelectorAll: vi.fn().mockReturnValue(mockSiblingElements), }; component['elementRef'] = { nativeElement: { - closest: jasmine - .createSpy('closest') - .and.returnValue(mockTableElement), + closest: vi.fn().mockReturnValue(mockTableElement), }, }; - spyOn(component, 'onSpace').and.stub(); - spyOn(component, 'onArrowDown').and.stub(); - spyOn(component, 'onArrowUp').and.stub(); - spyOn(component, 'onArrowRight').and.stub(); - spyOn(component, 'onArrowLeft').and.stub(); + vi.spyOn(component, 'onSpace').mockImplementation(() => {}); + vi.spyOn(component, 'onArrowDown').mockImplementation(() => {}); + vi.spyOn(component, 'onArrowUp').mockImplementation(() => {}); + vi.spyOn(component, 'onArrowRight').mockImplementation(() => {}); + vi.spyOn(component, 'onArrowLeft').mockImplementation(() => {}); component.onKeydown(mockSpaceEvent); expect(component.onSpace).toHaveBeenCalled(); @@ -158,23 +154,23 @@ describe('ToggleLinkCellComponent', () => { expect(component.onArrowLeft).toHaveBeenCalled(); }); - it('should make active item the only focusable item and navigate', fakeAsync(() => { + it('should make active item the only focusable item and navigate', () => { Object.defineProperty(mockSpaceEvent, 'target', { value: mockElement1, }); - spyOn(mockSpaceEvent, 'preventDefault'); + vi.spyOn(mockSpaceEvent, 'preventDefault'); component.onSpace(mockSpaceEvent, mockSiblingElements); expect(mockSpaceEvent.preventDefault).toHaveBeenCalled(); expect(mockElement1.tabIndex).toEqual(0); expect(mockElement2.tabIndex).toEqual(-1); - })); + }); it('should focus next link on ArrowDown', () => { const currentSelectedIndex = 0; - spyOn(mockArrowDownEvent, 'preventDefault'); - spyOn(mockElement2, 'focus'); + vi.spyOn(mockArrowDownEvent, 'preventDefault'); + vi.spyOn(mockElement2, 'focus'); component.onArrowDown( mockArrowDownEvent, @@ -188,8 +184,8 @@ describe('ToggleLinkCellComponent', () => { it('should focus previous element on ArrowUp', () => { const currentSelectedIndex = 1; - spyOn(mockArrowUpEvent, 'preventDefault'); - spyOn(mockElement1, 'focus'); + vi.spyOn(mockArrowUpEvent, 'preventDefault'); + vi.spyOn(mockElement1, 'focus'); component.onArrowUp( mockArrowUpEvent, @@ -206,7 +202,7 @@ describe('ToggleLinkCellComponent', () => { writable: true, value: false, }); - spyOn(component, 'toggleItem'); + vi.spyOn(component, 'toggleItem'); component.onArrowRight(mockArrowRightEvent); @@ -218,7 +214,7 @@ describe('ToggleLinkCellComponent', () => { writable: true, value: true, }); - spyOn(component, 'toggleItem'); + vi.spyOn(component, 'toggleItem'); component.onArrowLeft(mockArrowLeftEvent); diff --git a/feature-libs/organization/administration/components/unit/list/unit-list.component.spec.ts b/feature-libs/organization/administration/components/unit/list/unit-list.component.spec.ts index 8f04ce73504..c344254a850 100644 --- a/feature-libs/organization/administration/components/unit/list/unit-list.component.spec.ts +++ b/feature-libs/organization/administration/components/unit/list/unit-list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -17,7 +18,6 @@ import { OrgUnitService } from '@spartacus/organization/administration/core'; import { MockUrlPipe } from 'core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe'; import { UrlTestingModule } from 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module'; import { UnitTreeService } from '../services/unit-tree.service'; -import createSpy = jasmine.createSpy; @Component({ template: '', @@ -30,8 +30,8 @@ class MockListComponent { } class MockUnitTreeService { - expandAll = createSpy('expandAll'); - collapseAll = createSpy('collapseAll'); + expandAll = vi.fn(); + collapseAll = vi.fn(); } class MockOrgUnitService implements Partial { @@ -91,8 +91,8 @@ describe('UnitListComponent', () => { }); it('should render links', () => { - expect(expandAll.innerText).toEqual('orgUnit.tree.expandAll'); - expect(collapseAll.innerText).toEqual('orgUnit.tree.collapseAll'); + expect(expandAll.textContent?.trim()).toEqual('orgUnit.tree.expandAll'); + expect(collapseAll.textContent?.trim()).toEqual('orgUnit.tree.collapseAll'); }); it('should call expandAll', () => { diff --git a/feature-libs/organization/administration/components/unit/services/unit-item.service.spec.ts b/feature-libs/organization/administration/components/unit/services/unit-item.service.spec.ts index b5760fa8f53..126b9232684 100644 --- a/feature-libs/organization/administration/components/unit/services/unit-item.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/services/unit-item.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RoutingService } from '@spartacus/core'; @@ -9,7 +10,6 @@ import { EMPTY, of } from 'rxjs'; import { UnitFormService } from '../form/unit-form.service'; import { CurrentUnitService } from './current-unit.service'; import { UnitItemService } from './unit-item.service'; -import createSpy = jasmine.createSpy; const mockCode = 'u1'; class MockRoutingService { @@ -38,7 +38,7 @@ class MockUnitFormService {} class MockCurrentUnitService { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } @@ -66,26 +66,26 @@ describe('UnitItemService', () => { }); it('should load unit', () => { - spyOn(unitService, 'get').and.callThrough(); + vi.spyOn(unitService, 'get'); service.load('123').subscribe(); expect(unitService.get).toHaveBeenCalledWith('123'); }); it('should get unit from facade', () => { - spyOn(unitService, 'get').and.callThrough(); + vi.spyOn(unitService, 'get'); service.load('123').subscribe(); expect(unitService.get).toHaveBeenCalledWith('123'); }); it('should load unit on each request', () => { - spyOn(unitService, 'load').and.callThrough(); + vi.spyOn(unitService, 'load'); service.load('123').subscribe(); expect(unitService.load).toHaveBeenCalledWith('123'); }); it('should update existing unit', () => { - spyOn(unitService, 'update').and.callThrough(); - spyOn(unitService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(unitService, 'update'); + vi.spyOn(unitService, 'getLoadingStatus'); expect(service.save(form, 'existingCode')).toEqual(mockItemStatus); expect(unitService.update).toHaveBeenCalledWith('existingCode', { @@ -96,8 +96,8 @@ describe('UnitItemService', () => { }); it('should create new unit', () => { - spyOn(unitService, 'create').and.callThrough(); - spyOn(unitService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(unitService, 'create'); + vi.spyOn(unitService, 'getLoadingStatus'); expect(service.save(form)).toEqual(mockItemStatus); expect(unitService.create).toHaveBeenCalledWith({ @@ -109,7 +109,7 @@ describe('UnitItemService', () => { it('should launch unit detail route', () => { const routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.launchDetails({ name: 'foo bar' }); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'orgUnitDetails', diff --git a/feature-libs/organization/administration/components/unit/services/unit-list.service.spec.ts b/feature-libs/organization/administration/components/unit/services/unit-list.service.spec.ts index e2f8f5e5ea0..0c403e053d4 100644 --- a/feature-libs/organization/administration/components/unit/services/unit-list.service.spec.ts +++ b/feature-libs/organization/administration/components/unit/services/unit-list.service.spec.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel, FeatureToggles } from '@spartacus/core'; @@ -20,12 +21,10 @@ import { TREE_TOGGLE } from './unit-tree.model'; import { UnitTreeService } from './unit-tree.service'; import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/feature-toggles/testing'; -import createSpy = jasmine.createSpy; - function verifyExpandedAll({ values }: EntitiesModel) { expect(values.length).toEqual(7); values.forEach((element) => { - expect(element.expanded).toBeTrue(); + expect(element.expanded).toBe(true); }); } @@ -34,7 +33,7 @@ function verifyCollapsedAll({ values }: EntitiesModel) { expect(values.length).toEqual(1); expect(root.uid).toEqual(mockedTree.id); - expect(root.expanded).toBeFalse(); + expect(root.expanded).toBe(false); expect(root.depthLevel).toEqual(0); expect(root.count).toEqual(mockedTree.children.length); } @@ -314,8 +313,8 @@ export class MockTableService { export class MockUnitTreeService { treeToggle$ = treeToggle$.asObservable(); - initialize = createSpy('initialize'); - isExpanded = createSpy('isExpanded').and.returnValue(false); + initialize = vi.fn(); + isExpanded = vi.fn().mockReturnValue(false); } const mockFeatureToggles: FeatureToggles = { @@ -376,7 +375,7 @@ describe('UnitListService', () => { it('should get expanded all items structure', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(true); + treeService.isExpanded = vi.fn().mockReturnValue(true); service.getData().subscribe((table) => (result = table)); @@ -386,7 +385,7 @@ describe('UnitListService', () => { it('should automatically sort unit tree by name', () => { let result: EntitiesModel; mockTree$.next(mockedTreeBeforeConvert); - treeService.isExpanded = createSpy().and.returnValue(true); + treeService.isExpanded = vi.fn().mockReturnValue(true); service.getData().subscribe((table) => (result = table)); @@ -396,12 +395,12 @@ describe('UnitListService', () => { describe('isSearchEnabled', () => { it('should return true when enableB2BUnitSearch toggle is enabled', () => { featureToggles.enableB2BUnitSearch = true; - expect(service.isSearchEnabled()).toBeTrue(); + expect(service.isSearchEnabled()).toBe(true); }); it('should return false when enableB2BUnitSearch toggle is disabled', () => { featureToggles.enableB2BUnitSearch = false; - expect(service.isSearchEnabled()).toBeFalse(); + expect(service.isSearchEnabled()).toBe(false); }); }); @@ -429,9 +428,9 @@ describe('UnitListService', () => { // Result is root node (ancestor of match) expect(result.id).toEqual('Rustic'); // Root is in forceExpandIds as ancestor - expect(forceExpandIds.has('Rustic')).toBeTrue(); + expect(forceExpandIds.has('Rustic')).toBe(true); // 'Rustic Services' is NOT in forceExpandIds (it matched itself) - expect(forceExpandIds.has('Rustic Services')).toBeFalse(); + expect(forceExpandIds.has('Rustic Services')).toBe(false); // Only matching branch kept under root expect(result.children.length).toEqual(1); expect(result.children[0].id).toEqual('Rustic Services'); @@ -468,8 +467,8 @@ describe('UnitListService', () => { expect(result.id).toEqual('Rustic'); // Ancestors should be in forceExpandIds - expect(forceExpandIds.has('Rustic')).toBeTrue(); - expect(forceExpandIds.has('Rustic Services')).toBeTrue(); + expect(forceExpandIds.has('Rustic')).toBe(true); + expect(forceExpandIds.has('Rustic Services')).toBe(true); // Only the matching branch remains expect(result.children.length).toEqual(1); @@ -511,8 +510,8 @@ describe('UnitListService', () => { ); expect(result).toBeDefined(); - expect(forceExpandIds.has('Rustic')).toBeTrue(); - expect(forceExpandIds.has('Rustic Services')).toBeTrue(); + expect(forceExpandIds.has('Rustic')).toBe(true); + expect(forceExpandIds.has('Rustic Services')).toBe(true); }); it('should match by node id', () => { @@ -527,16 +526,16 @@ describe('UnitListService', () => { expect(result).toBeDefined(); // Ancestors should be force-expanded - expect(forceExpandIds.has('Rustic')).toBeTrue(); - expect(forceExpandIds.has('Rustic Retail')).toBeTrue(); - expect(forceExpandIds.has('Custom Retail')).toBeTrue(); + expect(forceExpandIds.has('Rustic')).toBe(true); + expect(forceExpandIds.has('Rustic Retail')).toBe(true); + expect(forceExpandIds.has('Custom Retail')).toBe(true); }); }); describe('convertListItem with forceExpandIds', () => { it('should force-expand nodes in forceExpandIds', () => { const forceExpandIds = new Set(['Rustic', 'Rustic Services']); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); const result = (service as any).convertListItem( mockedTree, @@ -549,19 +548,19 @@ describe('UnitListService', () => { // Root 'Rustic' should be expanded via forceExpandIds const root = result.values[0]; expect(root.uid).toEqual('Rustic'); - expect(root.expanded).toBeTrue(); + expect(root.expanded).toBe(true); // 'Rustic Services' should also be expanded via forceExpandIds const rusticServices = result.values.find( (v: B2BUnitTreeNode) => v.uid === 'Rustic Services' ); expect(rusticServices).toBeDefined(); - expect(rusticServices.expanded).toBeTrue(); + expect(rusticServices.expanded).toBe(true); }); it('should not force-expand nodes NOT in forceExpandIds', () => { const forceExpandIds = new Set(['Rustic']); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); const result = (service as any).convertListItem( mockedTree, @@ -572,25 +571,25 @@ describe('UnitListService', () => { // Root should be expanded (in forceExpandIds) const root = result.values[0]; - expect(root.expanded).toBeTrue(); + expect(root.expanded).toBe(true); // 'Rustic Retail' is NOT in forceExpandIds and isExpanded returns false const rusticRetail = result.values.find( (v: B2BUnitTreeNode) => v.uid === 'Rustic Retail' ); expect(rusticRetail).toBeDefined(); - expect(rusticRetail.expanded).toBeFalse(); + expect(rusticRetail.expanded).toBe(false); }); it('should fall back to unitTreeService.isExpanded when no forceExpandIds', () => { - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); const result = (service as any).convertListItem(mockedTree); expect(result).toBeDefined(); // Without forceExpandIds, all nodes use isExpanded (returns false) const root = result.values[0]; - expect(root.expanded).toBeFalse(); + expect(root.expanded).toBe(false); expect(result.values.length).toEqual(1); // Only root since collapsed }); }); @@ -599,7 +598,7 @@ describe('UnitListService', () => { it('should filter tree when query is non-empty', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -613,9 +612,9 @@ describe('UnitListService', () => { // Visible: Rustic (expanded), Rustic Services (expanded), Services West expect(result.values.length).toEqual(3); expect(result.values[0].uid).toEqual('Rustic'); - expect(result.values[0].expanded).toBeTrue(); + expect(result.values[0].expanded).toBe(true); expect(result.values[1].uid).toEqual('Rustic Services'); - expect(result.values[1].expanded).toBeTrue(); + expect(result.values[1].expanded).toBe(true); expect(result.values[2].uid).toEqual('Services West'); expect(result.pagination.totalResults).toEqual(3); }); @@ -623,7 +622,7 @@ describe('UnitListService', () => { it('should filter when query is non-empty (min-char check is handled by component pipe)', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -640,7 +639,7 @@ describe('UnitListService', () => { it('should not filter when query is empty', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -655,7 +654,7 @@ describe('UnitListService', () => { it('should not filter when query is only whitespace', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -670,7 +669,7 @@ describe('UnitListService', () => { it('should return empty result when no search results', () => { let result: EntitiesModel | undefined; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -686,7 +685,7 @@ describe('UnitListService', () => { it('should show parent match with all children preserved', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -698,9 +697,9 @@ describe('UnitListService', () => { expect(result).toBeDefined(); // Rustic (ancestor, force-expanded) -> Rustic Services (self-match, NOT force-expanded, uses isExpanded=false) expect(result.values[0].uid).toEqual('Rustic'); - expect(result.values[0].expanded).toBeTrue(); // forceExpandIds + expect(result.values[0].expanded).toBe(true); // forceExpandIds expect(result.values[1].uid).toEqual('Rustic Services'); - expect(result.values[1].expanded).toBeFalse(); // self-match, isExpanded returns false + expect(result.values[1].expanded).toBe(false); // self-match, isExpanded returns false // Total: 2 visible nodes (Rustic Services collapsed, its children hidden) expect(result.values.length).toEqual(2); // But Rustic Services still has children count @@ -710,7 +709,7 @@ describe('UnitListService', () => { it('should trim and lowercase query before filtering', () => { let result: EntitiesModel; mockTree$.next(mockedTree); - treeService.isExpanded = createSpy().and.returnValue(false); + treeService.isExpanded = vi.fn().mockReturnValue(false); service.getData().subscribe((table) => { result = table; @@ -722,16 +721,16 @@ describe('UnitListService', () => { // Should find 'Services West' despite leading/trailing spaces and casing expect( result.values.some((v: B2BUnitTreeNode) => v.uid === 'Services West') - ).toBeTrue(); + ).toBe(true); }); it('should preserve expand state when search is cleared', () => { // Set manual expansion: only root expanded - treeService.isExpanded = createSpy().and.callFake( - (id: string, _level: number) => { + treeService.isExpanded = vi + .fn() + .mockImplementation((id: string, _level: number) => { return id === 'Rustic'; - } - ); + }); let result: EntitiesModel; mockTree$.next(mockedTree); @@ -749,11 +748,11 @@ describe('UnitListService', () => { // Root is expanded (manual state), children visible but collapsed expect(result.values[0].uid).toEqual('Rustic'); - expect(result.values[0].expanded).toBeTrue(); + expect(result.values[0].expanded).toBe(true); // Root's children should be visible since root is expanded expect(result.values.length).toEqual(3); // Rustic + 2 children (both collapsed) - expect(result.values[1].expanded).toBeFalse(); - expect(result.values[2].expanded).toBeFalse(); + expect(result.values[1].expanded).toBe(false); + expect(result.values[2].expanded).toBe(false); }); }); }); diff --git a/feature-libs/organization/administration/components/user-group/details/user-group-details.component.spec.ts b/feature-libs/organization/administration/components/user-group/details/user-group-details.component.spec.ts index 69c7d200c34..c161e7448b1 100644 --- a/feature-libs/organization/administration/components/user-group/details/user-group-details.component.spec.ts +++ b/feature-libs/organization/administration/components/user-group/details/user-group-details.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Directive, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -22,13 +23,12 @@ import { CardTestingModule } from '../../shared/card/card.testing.module'; import { ItemService } from '../../shared/item.service'; import { MessageService } from '../../shared/message/services/message.service'; import { UserGroupDetailsComponent } from './user-group-details.component'; -import createSpy = jasmine.createSpy; const mockCode = 'u1'; class MockUserGroupItemService implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } diff --git a/feature-libs/organization/administration/components/user-group/form/user-group-form.component.spec.ts b/feature-libs/organization/administration/components/user-group/form/user-group-form.component.spec.ts index 407d5ee9b50..3301beb9f99 100644 --- a/feature-libs/organization/administration/components/user-group/form/user-group-form.component.spec.ts +++ b/feature-libs/organization/administration/components/user-group/form/user-group-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, @@ -95,8 +96,8 @@ describe('UserGroupFormComponent', () => { b2bUnitService = TestBed.inject(OrgUnitService); - spyOn(b2bUnitService, 'getActiveUnitList').and.callThrough(); - spyOn(b2bUnitService, 'loadList').and.callThrough(); + vi.spyOn(b2bUnitService, 'getActiveUnitList'); + vi.spyOn(b2bUnitService, 'loadList'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/user-group/permissions/user-group-permission-list.service.spec.ts b/feature-libs/organization/administration/components/user-group/permissions/user-group-permission-list.service.spec.ts index 84f7cb58710..976ccf23955 100644 --- a/feature-libs/organization/administration/components/user-group/permissions/user-group-permission-list.service.spec.ts +++ b/feature-libs/organization/administration/components/user-group/permissions/user-group-permission-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel } from '@spartacus/core'; @@ -95,8 +96,8 @@ describe('UserGroupPermissionListService', () => { }); it('should assign permission', () => { - spyOn(userGroupService, 'assignPermission').and.callThrough(); - spyOn(permissionService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userGroupService, 'assignPermission'); + vi.spyOn(permissionService, 'getLoadingStatus'); expect(service.assign('userGroupCode', 'permissionCode')).toEqual( mockItemStatus @@ -111,8 +112,8 @@ describe('UserGroupPermissionListService', () => { }); it('should unassign permission', () => { - spyOn(userGroupService, 'unassignPermission').and.callThrough(); - spyOn(permissionService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userGroupService, 'unassignPermission'); + vi.spyOn(permissionService, 'getLoadingStatus'); expect(service.unassign('userGroupCode', 'permissionCode')).toEqual( mockItemStatus diff --git a/feature-libs/organization/administration/components/user-group/users/user-group-user-list.component.spec.ts b/feature-libs/organization/administration/components/user-group/users/user-group-user-list.component.spec.ts index d01efe9572d..9eb0a024490 100644 --- a/feature-libs/organization/administration/components/user-group/users/user-group-user-list.component.spec.ts +++ b/feature-libs/organization/administration/components/user-group/users/user-group-user-list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; @@ -83,7 +84,7 @@ describe('UserGroupUserListComponent', () => { }); it('should unassign all members', () => { - spyOn(userGroupUserListService, 'unassignAllMembers').and.callThrough(); + vi.spyOn(userGroupUserListService, 'unassignAllMembers'); component.unassignAll(); expect(userGroupUserListService.unassignAllMembers).toHaveBeenCalledWith( @@ -92,7 +93,7 @@ describe('UserGroupUserListComponent', () => { }); it('should notify after unassign all members', () => { - spyOn(component.subList.messageService, 'add').and.callThrough(); + vi.spyOn(component.subList.messageService, 'add'); component.unassignAll(); expect(component.subList.messageService.add).toHaveBeenCalledWith({ diff --git a/feature-libs/organization/administration/components/user-group/users/user-group-user-list.service.spec.ts b/feature-libs/organization/administration/components/user-group/users/user-group-user-list.service.spec.ts index 9fb75b6ecfd..15416d2b586 100644 --- a/feature-libs/organization/administration/components/user-group/users/user-group-user-list.service.spec.ts +++ b/feature-libs/organization/administration/components/user-group/users/user-group-user-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { B2BUser, EntitiesModel } from '@spartacus/core'; @@ -91,8 +92,8 @@ describe('UserGroupUserListService', () => { }); it('should assign permission', () => { - spyOn(userGroupService, 'assignMember').and.callThrough(); - spyOn(userService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userGroupService, 'assignMember'); + vi.spyOn(userService, 'getLoadingStatus'); expect(service.assign('userGroupCode', 'customerId')).toEqual( mockItemStatus @@ -105,8 +106,8 @@ describe('UserGroupUserListService', () => { }); it('should unassign permission', () => { - spyOn(userGroupService, 'unassignMember').and.callThrough(); - spyOn(userService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userGroupService, 'unassignMember'); + vi.spyOn(userService, 'getLoadingStatus'); expect(service.unassign('userGroupCode', 'customerId')).toEqual( mockItemStatus diff --git a/feature-libs/organization/administration/components/user/approvers/user-approver-list.service.spec.ts b/feature-libs/organization/administration/components/user/approvers/user-approver-list.service.spec.ts index f566441221f..45f5d36c3cd 100644 --- a/feature-libs/organization/administration/components/user/approvers/user-approver-list.service.spec.ts +++ b/feature-libs/organization/administration/components/user/approvers/user-approver-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { B2BUser, EntitiesModel } from '@spartacus/core'; @@ -95,8 +96,8 @@ describe('UserApproverListService', () => { }); it('should assign approver', () => { - spyOn(userService, 'assignApprover').and.callThrough(); - spyOn(userService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userService, 'assignApprover'); + vi.spyOn(userService, 'getLoadingStatus'); expect(service.assign('customerId', 'approverId')).toEqual(mockItemStatus); expect(userService.assignApprover).toHaveBeenCalledWith( @@ -107,8 +108,8 @@ describe('UserApproverListService', () => { }); it('should unassign approver', () => { - spyOn(userService, 'unassignApprover').and.callThrough(); - spyOn(userService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userService, 'unassignApprover'); + vi.spyOn(userService, 'getLoadingStatus'); expect(service.unassign('customerId', 'approverId')).toEqual( mockItemStatus diff --git a/feature-libs/organization/administration/components/user/change-password-form/user-change-password-form.component.spec.ts b/feature-libs/organization/administration/components/user/change-password-form/user-change-password-form.component.spec.ts index 2c041f24dc8..474bd279af1 100644 --- a/feature-libs/organization/administration/components/user/change-password-form/user-change-password-form.component.spec.ts +++ b/feature-libs/organization/administration/components/user/change-password-form/user-change-password-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Directive, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { @@ -117,14 +118,14 @@ describe('UserChangePasswordFormComponent', () => { }); it('should render the form', () => { - spyOn(formService, 'getForm').and.returnValue(mockForm); + vi.spyOn(formService, 'getForm').mockReturnValue(mockForm); fixture.detectChanges(); const form = fixture.debugElement.queryAll(By.css('form input')); expect(form.length).toEqual(2); }); it('should not render any form groups if the form is falsy', () => { - spyOn(formService, 'getForm').and.returnValue(undefined); + vi.spyOn(formService, 'getForm').mockReturnValue(undefined); fixture.detectChanges(); const form = fixture.debugElement.query(By.css('form')); expect(form).toBeNull(); diff --git a/feature-libs/organization/administration/components/user/details-cell/user-details-cell.component.spec.ts b/feature-libs/organization/administration/components/user/details-cell/user-details-cell.component.spec.ts index f0efe1de74f..e61985d7b1c 100644 --- a/feature-libs/organization/administration/components/user/details-cell/user-details-cell.component.spec.ts +++ b/feature-libs/organization/administration/components/user/details-cell/user-details-cell.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { B2BUser, B2BUserRight, B2BUserRole } from '@spartacus/core'; import { B2BUserService } from '@spartacus/organization/administration/core'; @@ -47,8 +48,8 @@ describe('RolesCellComponent', () => { b2bUserService = TestBed.inject(B2BUserService); - spyOn(b2bUserService, 'getAllRights').and.callThrough(); - spyOn(b2bUserService, 'getAllRoles').and.callThrough(); + vi.spyOn(b2bUserService, 'getAllRights'); + vi.spyOn(b2bUserService, 'getAllRoles'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/user/details/user-details.component.spec.ts b/feature-libs/organization/administration/components/user/details/user-details.component.spec.ts index 970a73fca9f..36b0f77bff5 100644 --- a/feature-libs/organization/administration/components/user/details/user-details.component.spec.ts +++ b/feature-libs/organization/administration/components/user/details/user-details.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Directive, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -32,7 +33,6 @@ import { ItemService } from '../../shared/item.service'; import { MessageTestingModule } from '../../shared/message/message.testing.module'; import { MessageService } from '../../shared/message/services/message.service'; import { UserDetailsComponent } from './user-details.component'; -import createSpy = jasmine.createSpy; const mockCode = 'c1'; @@ -46,7 +46,7 @@ const mockB2BUserWithoutRight: B2BUser = { class MockUserItemService implements Partial> { key$ = of(mockCode); - load = createSpy('load').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(EMPTY); error$ = of(false); } @@ -135,9 +135,9 @@ describe('UserDetailsComponent', () => { b2bUserService = TestBed.inject(B2BUserService); - spyOn(b2bUserService, 'getAllRights').and.callThrough(); - spyOn(b2bUserService, 'getAllRoles').and.callThrough(); - spyOn(b2bUserService, 'isUpdatingUserAllowed').and.callThrough(); + vi.spyOn(b2bUserService, 'getAllRights'); + vi.spyOn(b2bUserService, 'getAllRoles'); + vi.spyOn(b2bUserService, 'isUpdatingUserAllowed'); fixture = TestBed.createComponent(UserDetailsComponent); itemService = fixture.componentRef.injector.get(ItemService); diff --git a/feature-libs/organization/administration/components/user/form/user-form.component.spec.ts b/feature-libs/organization/administration/components/user/form/user-form.component.spec.ts index fa25b05cb04..f5e6fd14c27 100644 --- a/feature-libs/organization/administration/components/user/form/user-form.component.spec.ts +++ b/feature-libs/organization/administration/components/user/form/user-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, @@ -129,13 +130,13 @@ describe('UserFormComponent', () => { b2bUnitService = TestBed.inject(OrgUnitService); - spyOn(b2bUnitService, 'getActiveUnitList').and.callThrough(); - spyOn(b2bUnitService, 'loadList').and.callThrough(); + vi.spyOn(b2bUnitService, 'getActiveUnitList'); + vi.spyOn(b2bUnitService, 'loadList'); b2bUserService = TestBed.inject(B2BUserService); - spyOn(b2bUserService, 'getAllRights').and.callThrough(); - spyOn(b2bUserService, 'getAllRoles').and.callThrough(); + vi.spyOn(b2bUserService, 'getAllRights'); + vi.spyOn(b2bUserService, 'getAllRoles'); }); beforeEach(() => { diff --git a/feature-libs/organization/administration/components/user/permissions/user-permission-list.service.spec.ts b/feature-libs/organization/administration/components/user/permissions/user-permission-list.service.spec.ts index 71af7dbdd73..95f0e91fc1d 100644 --- a/feature-libs/organization/administration/components/user/permissions/user-permission-list.service.spec.ts +++ b/feature-libs/organization/administration/components/user/permissions/user-permission-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel } from '@spartacus/core'; @@ -93,8 +94,8 @@ describe('UserPermissionListService', () => { }); it('should assign permission', () => { - spyOn(userService, 'assignPermission').and.callThrough(); - spyOn(permissionService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userService, 'assignPermission'); + vi.spyOn(permissionService, 'getLoadingStatus'); expect(service.assign('customerId', 'permissionCode')).toEqual( mockItemStatus @@ -109,8 +110,8 @@ describe('UserPermissionListService', () => { }); it('should unassign permission', () => { - spyOn(userService, 'unassignPermission').and.callThrough(); - spyOn(permissionService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userService, 'unassignPermission'); + vi.spyOn(permissionService, 'getLoadingStatus'); expect(service.unassign('customerId', 'permissionCode')).toEqual( mockItemStatus diff --git a/feature-libs/organization/administration/components/user/user-groups/user-user-group-list.service.spec.ts b/feature-libs/organization/administration/components/user/user-groups/user-user-group-list.service.spec.ts index dcb40861bd6..50a3e82c6b2 100644 --- a/feature-libs/organization/administration/components/user/user-groups/user-user-group-list.service.spec.ts +++ b/feature-libs/organization/administration/components/user/user-groups/user-user-group-list.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EntitiesModel } from '@spartacus/core'; @@ -94,8 +95,8 @@ describe('UserUserGroupListService', () => { }); it('should assign permission', () => { - spyOn(userService, 'assignUserGroup'); - spyOn(userGroupService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userService, 'assignUserGroup'); + vi.spyOn(userGroupService, 'getLoadingStatus'); expect(service.assign('customerId', 'userGroupUid')).toEqual( mockItemStatus @@ -110,8 +111,8 @@ describe('UserUserGroupListService', () => { }); it('should unassign permission', () => { - spyOn(userService, 'unassignUserGroup').and.callThrough(); - spyOn(userGroupService, 'getLoadingStatus').and.callThrough(); + vi.spyOn(userService, 'unassignUserGroup'); + vi.spyOn(userGroupService, 'getLoadingStatus'); expect(service.unassign('customerId', 'userGroupUid')).toEqual( mockItemStatus diff --git a/feature-libs/organization/administration/core/connectors/b2b-user/b2b-user.connector.spec.ts b/feature-libs/organization/administration/core/connectors/b2b-user/b2b-user.connector.spec.ts index b0a0cabb804..0dbb48042cd 100644 --- a/feature-libs/organization/administration/core/connectors/b2b-user/b2b-user.connector.spec.ts +++ b/feature-libs/organization/administration/core/connectors/b2b-user/b2b-user.connector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { B2BUser, SearchConfig } from '@spartacus/core'; @@ -9,8 +10,6 @@ import { of } from 'rxjs'; import { B2BUserAdapter } from './b2b-user.adapter'; import { B2BUserConnector } from './b2b-user.connector'; -import createSpy = jasmine.createSpy; - const customerId = 'userId'; const approverId = 'approverId'; const permissionId = 'permissionId'; @@ -30,40 +29,20 @@ const userGroup: UserGroup = { }; class MockB2BUserAdapter implements B2BUserAdapter { - load = createSpy('B2BUserAdapter.load').and.returnValue(of(b2bUser)); - loadList = createSpy('B2BUserAdapter.loadList').and.returnValue( - of([b2bUser]) - ); - create = createSpy('B2BUserAdapter.create').and.returnValue(of(b2bUser)); - update = createSpy('B2BUserAdapter.update').and.returnValue(of(b2bUser)); - - loadApprovers = createSpy('B2BUserAdapter.loadApprovers').and.returnValue( - of([b2bUser]) - ); - assignApprover = createSpy('B2BUserAdapter.assignApprover').and.returnValue( - of(b2bUser) - ); - unassignApprover = createSpy( - 'B2BUserAdapter.unassignApprover' - ).and.returnValue(of(b2bUser)); - loadPermissions = createSpy('B2BUserAdapter.loadPermissions').and.returnValue( - of([permission]) - ); - assignPermission = createSpy( - 'B2BUserAdapter.assignPermission' - ).and.returnValue(of(b2bUser)); - unassignPermission = createSpy( - 'B2BUserAdapter.unassignPermission' - ).and.returnValue(of(b2bUser)); - loadUserGroups = createSpy('B2BUserAdapter.loadUserGroups').and.returnValue( - of([userGroup]) - ); - assignUserGroup = createSpy('B2BUserAdapter.assignUserGroup').and.returnValue( - of(userGroup) - ); - unassignUserGroup = createSpy( - 'B2BUserAdapter.unassignUserGroup' - ).and.returnValue(of(userGroup)); + load = vi.fn().mockReturnValue(of(b2bUser)); + loadList = vi.fn().mockReturnValue(of([b2bUser])); + create = vi.fn().mockReturnValue(of(b2bUser)); + update = vi.fn().mockReturnValue(of(b2bUser)); + + loadApprovers = vi.fn().mockReturnValue(of([b2bUser])); + assignApprover = vi.fn().mockReturnValue(of(b2bUser)); + unassignApprover = vi.fn().mockReturnValue(of(b2bUser)); + loadPermissions = vi.fn().mockReturnValue(of([permission])); + assignPermission = vi.fn().mockReturnValue(of(b2bUser)); + unassignPermission = vi.fn().mockReturnValue(of(b2bUser)); + loadUserGroups = vi.fn().mockReturnValue(of([userGroup])); + assignUserGroup = vi.fn().mockReturnValue(of(userGroup)); + unassignUserGroup = vi.fn().mockReturnValue(of(userGroup)); } describe('B2BUserConnector', () => { diff --git a/feature-libs/organization/administration/core/connectors/budget/budget.connector.spec.ts b/feature-libs/organization/administration/core/connectors/budget/budget.connector.spec.ts index 234a3516ee0..9cab5ceea21 100644 --- a/feature-libs/organization/administration/core/connectors/budget/budget.connector.spec.ts +++ b/feature-libs/organization/administration/core/connectors/budget/budget.connector.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { SearchConfig } from '@spartacus/core'; import { of } from 'rxjs'; import { BudgetAdapter } from './budget.adapter'; import { BudgetConnector } from './budget.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId'; const budgetCode = 'budgetCode'; @@ -13,10 +13,10 @@ const budget = { }; class MockBudgetAdapter implements BudgetAdapter { - load = createSpy('BudgetAdapter.load').and.returnValue(of(budget)); - loadList = createSpy('BudgetAdapter.loadList').and.returnValue(of([budget])); - create = createSpy('BudgetAdapter.create').and.returnValue(of(budget)); - update = createSpy('BudgetAdapter.update').and.returnValue(of(budget)); + load = vi.fn().mockReturnValue(of(budget)); + loadList = vi.fn().mockReturnValue(of([budget])); + create = vi.fn().mockReturnValue(of(budget)); + update = vi.fn().mockReturnValue(of(budget)); } describe('BudgetConnector', () => { diff --git a/feature-libs/organization/administration/core/connectors/cost-center/cost-center.connector.spec.ts b/feature-libs/organization/administration/core/connectors/cost-center/cost-center.connector.spec.ts index 5b6764275b7..5be97e95e6f 100644 --- a/feature-libs/organization/administration/core/connectors/cost-center/cost-center.connector.spec.ts +++ b/feature-libs/organization/administration/core/connectors/cost-center/cost-center.connector.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { SearchConfig } from '@spartacus/core'; import { of } from 'rxjs'; import { CostCenterAdapter } from './cost-center.adapter'; import { CostCenterConnector } from './cost-center.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId'; const costCenterCode = 'costCenterCode'; @@ -18,21 +18,13 @@ const budget = { }; class MockCostCenterAdapter implements CostCenterAdapter { - load = createSpy('CostCenterAdapter.load').and.returnValue(of(costCenter)); - loadList = createSpy('CostCenterAdapter.loadList').and.returnValue( - of([costCenter]) - ); - create = createSpy('CostCenterAdapter.create').and.returnValue( - of(costCenter) - ); - update = createSpy('CostCenterAdapter.update').and.returnValue( - of(costCenter) - ); - loadBudgets = createSpy('CostCenterAdapter.loadBudgets').and.returnValue( - of([budget]) - ); - assignBudget = createSpy('CostCenterAdapter.assignBudget'); - unassignBudget = createSpy('CostCenterAdapter.unassignBudget'); + load = vi.fn().mockReturnValue(of(costCenter)); + loadList = vi.fn().mockReturnValue(of([costCenter])); + create = vi.fn().mockReturnValue(of(costCenter)); + update = vi.fn().mockReturnValue(of(costCenter)); + loadBudgets = vi.fn().mockReturnValue(of([budget])); + assignBudget = vi.fn(); + unassignBudget = vi.fn(); } describe('CostCenterConnector', () => { diff --git a/feature-libs/organization/administration/core/connectors/org-unit/org-unit.connector.spec.ts b/feature-libs/organization/administration/core/connectors/org-unit/org-unit.connector.spec.ts index a7bea2b9aed..b78be24049b 100644 --- a/feature-libs/organization/administration/core/connectors/org-unit/org-unit.connector.spec.ts +++ b/feature-libs/organization/administration/core/connectors/org-unit/org-unit.connector.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { Address, B2BApprovalProcess, SearchConfig } from '@spartacus/core'; import { EMPTY, of } from 'rxjs'; import { OrgUnitAdapter } from './org-unit.adapter'; import { OrgUnitConnector } from './org-unit.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId'; const orgUnitId = 'orgUnitId'; @@ -28,23 +28,21 @@ const approvalProcess: B2BApprovalProcess = { }; class MockOrgUnitAdapter implements OrgUnitAdapter { - load = createSpy('load').and.returnValue(of(orgUnit)); - loadList = createSpy('loadList').and.returnValue(of([orgUnitNode])); - create = createSpy('create').and.returnValue(of(orgUnit)); - update = createSpy('update').and.returnValue(of(orgUnit)); - loadTree = createSpy('loadTree').and.returnValue(of(orgUnit)); - loadApprovalProcesses = createSpy('loadApprovalProcesses').and.returnValue( - of([approvalProcess]) - ); - loadUsers = createSpy('loadUsers').and.returnValue(EMPTY); - assignRole = createSpy('assignRole').and.returnValue(EMPTY); - unassignRole = createSpy('unassignRole').and.returnValue(EMPTY); - assignApprover = createSpy('assignApprover').and.returnValue(EMPTY); - unassignApprover = createSpy('unassignApprover').and.returnValue(EMPTY); - loadAddresses = createSpy('loadAddresses').and.returnValue(EMPTY); - createAddress = createSpy('createAddress').and.returnValue(EMPTY); - updateAddress = createSpy('updateAddress').and.returnValue(EMPTY); - deleteAddress = createSpy('deleteAddress').and.returnValue(EMPTY); + load = vi.fn().mockReturnValue(of(orgUnit)); + loadList = vi.fn().mockReturnValue(of([orgUnitNode])); + create = vi.fn().mockReturnValue(of(orgUnit)); + update = vi.fn().mockReturnValue(of(orgUnit)); + loadTree = vi.fn().mockReturnValue(of(orgUnit)); + loadApprovalProcesses = vi.fn().mockReturnValue(of([approvalProcess])); + loadUsers = vi.fn().mockReturnValue(EMPTY); + assignRole = vi.fn().mockReturnValue(EMPTY); + unassignRole = vi.fn().mockReturnValue(EMPTY); + assignApprover = vi.fn().mockReturnValue(EMPTY); + unassignApprover = vi.fn().mockReturnValue(EMPTY); + loadAddresses = vi.fn().mockReturnValue(EMPTY); + createAddress = vi.fn().mockReturnValue(EMPTY); + updateAddress = vi.fn().mockReturnValue(EMPTY); + deleteAddress = vi.fn().mockReturnValue(EMPTY); } describe('OrgUnitConnector', () => { diff --git a/feature-libs/organization/administration/core/connectors/permission/permission.connector.spec.ts b/feature-libs/organization/administration/core/connectors/permission/permission.connector.spec.ts index c43fbf432bc..3d1fba77277 100644 --- a/feature-libs/organization/administration/core/connectors/permission/permission.connector.spec.ts +++ b/feature-libs/organization/administration/core/connectors/permission/permission.connector.spec.ts @@ -1,11 +1,10 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { SearchConfig, OrderApprovalPermissionType } from '@spartacus/core'; import { of } from 'rxjs'; import { PermissionAdapter } from './permission.adapter'; import { PermissionConnector } from './permission.connector'; -import createSpy = jasmine.createSpy; - const userId = 'userId'; const permissionCode = 'permissionCode'; @@ -16,19 +15,11 @@ const permission = { const types: OrderApprovalPermissionType[] = [{ code: 'test', name: 'name' }]; class MockPermissionAdapter implements PermissionAdapter { - load = createSpy('PermissionAdapter.load').and.returnValue(of(permission)); - loadList = createSpy('PermissionAdapter.loadList').and.returnValue( - of([permission]) - ); - create = createSpy('PermissionAdapter.create').and.returnValue( - of(permission) - ); - update = createSpy('PermissionAdapter.update').and.returnValue( - of(permission) - ); - loadTypes = createSpy('PermissionAdapter.loadTypes').and.returnValue( - of(types) - ); + load = vi.fn().mockReturnValue(of(permission)); + loadList = vi.fn().mockReturnValue(of([permission])); + create = vi.fn().mockReturnValue(of(permission)); + update = vi.fn().mockReturnValue(of(permission)); + loadTypes = vi.fn().mockReturnValue(of(types)); } describe('PermissionConnector', () => { diff --git a/feature-libs/organization/administration/core/connectors/user-group/user-group.connector.spec.ts b/feature-libs/organization/administration/core/connectors/user-group/user-group.connector.spec.ts index c31b1d26382..f8f310d809a 100644 --- a/feature-libs/organization/administration/core/connectors/user-group/user-group.connector.spec.ts +++ b/feature-libs/organization/administration/core/connectors/user-group/user-group.connector.spec.ts @@ -1,10 +1,10 @@ +import { vi } from 'vitest'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { SearchConfig } from '@spartacus/core'; import { of } from 'rxjs'; import { UserGroupAdapter } from './user-group.adapter'; import { UserGroupConnector } from './user-group.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId'; const userGroupId = 'userGroupId'; @@ -22,24 +22,20 @@ const member = { }; class MockUserGroupAdapter implements UserGroupAdapter { - load = createSpy('load').and.returnValue(of(userGroup)); - loadList = createSpy('loadList').and.returnValue(of([userGroup])); - create = createSpy('create').and.returnValue(of(userGroup)); - update = createSpy('update').and.returnValue(of(userGroup)); - delete = createSpy('delete').and.returnValue(of(userGroup)); - loadAvailableOrderApprovalPermissions = createSpy( - 'loadAvailableOrderApprovalPermissions' - ).and.returnValue(of([permission])); - loadAvailableOrgCustomers = createSpy( - 'loadAvailableOrgCustomers' - ).and.returnValue(of([member])); - assignMember = createSpy('assignMember'); - assignOrderApprovalPermission = createSpy('assignOrderApprovalPermission'); - unassignMember = createSpy('unassignMember'); - unassignAllMembers = createSpy('unassignAllMembers'); - unassignOrderApprovalPermission = createSpy( - 'unassignOrderApprovalPermission' - ); + load = vi.fn().mockReturnValue(of(userGroup)); + loadList = vi.fn().mockReturnValue(of([userGroup])); + create = vi.fn().mockReturnValue(of(userGroup)); + update = vi.fn().mockReturnValue(of(userGroup)); + delete = vi.fn().mockReturnValue(of(userGroup)); + loadAvailableOrderApprovalPermissions = vi + .fn() + .mockReturnValue(of([permission])); + loadAvailableOrgCustomers = vi.fn().mockReturnValue(of([member])); + assignMember = vi.fn(); + assignOrderApprovalPermission = vi.fn(); + unassignMember = vi.fn(); + unassignAllMembers = vi.fn(); + unassignOrderApprovalPermission = vi.fn(); } describe('UserGroupConnector', () => { diff --git a/feature-libs/organization/administration/core/guards/admin.guard.spec.ts b/feature-libs/organization/administration/core/guards/admin.guard.spec.ts index fd33d20d869..bdef6914434 100644 --- a/feature-libs/organization/administration/core/guards/admin.guard.spec.ts +++ b/feature-libs/organization/administration/core/guards/admin.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { B2BUserRole, @@ -9,7 +10,6 @@ import { import { UserAccountFacade } from '@spartacus/user/account/root'; import { of } from 'rxjs'; import { AdminGuard } from './admin.guard'; -import createSpy = jasmine.createSpy; const mockUserDetails: User = { firstName: 'test', @@ -18,15 +18,15 @@ const mockUserDetails: User = { }; class MockUserAccountFacade implements Partial { - get = createSpy('get').and.returnValue(of(mockUserDetails)); + get = vi.fn().mockReturnValue(of(mockUserDetails)); } class MockRoutingService implements Partial { - go = createSpy('go'); + go = vi.fn(); } class MockGlobalMessageService implements Partial { - add = createSpy('add'); + add = vi.fn(); } describe('AdminGuard', () => { diff --git a/feature-libs/organization/administration/core/guards/org-unit.guard.spec.ts b/feature-libs/organization/administration/core/guards/org-unit.guard.spec.ts index 60288b5cdd1..ee0207734d2 100644 --- a/feature-libs/organization/administration/core/guards/org-unit.guard.spec.ts +++ b/feature-libs/organization/administration/core/guards/org-unit.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UrlTree } from '@angular/router'; import { StoreModule } from '@ngrx/store'; @@ -8,10 +9,9 @@ import { } from '@spartacus/core'; import { OrgUnitService } from '../services'; import { OrgUnitGuard } from './org-unit.guard'; -import createSpy = jasmine.createSpy; class MockGlobalMessageService implements Partial { - add = createSpy('add'); + add = vi.fn(); } class MockOrgUnitService implements Partial { @@ -58,14 +58,14 @@ describe('OrgUnitGuard', () => { describe('canActivate()', () => { it('should return true when updating unit is allowed', () => { let result: boolean | UrlTree; - spyOn(orgUnitService, 'isUpdatingUnitAllowed').and.returnValue(true); + vi.spyOn(orgUnitService, 'isUpdatingUnitAllowed').mockReturnValue(true); result = guard.canActivate(); expect(result).toEqual(true); }); it('should return organization url for redirection when updating unit is not allowed', () => { let result: boolean | UrlTree; - spyOn(orgUnitService, 'isUpdatingUnitAllowed').and.returnValue(false); + vi.spyOn(orgUnitService, 'isUpdatingUnitAllowed').mockReturnValue(false); result = guard.canActivate(); expect(result.toString()).toBe('/organization'); expect(globalMessageService.add).toHaveBeenCalledWith( diff --git a/feature-libs/organization/administration/core/guards/user.guard.spec.ts b/feature-libs/organization/administration/core/guards/user.guard.spec.ts index 090ea77a12f..1436079764d 100644 --- a/feature-libs/organization/administration/core/guards/user.guard.spec.ts +++ b/feature-libs/organization/administration/core/guards/user.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UrlTree } from '@angular/router'; import { StoreModule } from '@ngrx/store'; @@ -8,10 +9,9 @@ import { } from '@spartacus/core'; import { B2BUserService } from '../services'; import { UserGuard } from './user.guard'; -import createSpy = jasmine.createSpy; class MockGlobalMessageService implements Partial { - add = createSpy('add'); + add = vi.fn(); } class MockB2BUserService implements Partial { @@ -58,14 +58,14 @@ describe('UserGuard', () => { describe('canActivate()', () => { it('should return true when updating user is allowed', () => { let result: boolean | UrlTree; - spyOn(b2bUserService, 'isUpdatingUserAllowed').and.returnValue(true); + vi.spyOn(b2bUserService, 'isUpdatingUserAllowed').mockReturnValue(true); result = guard.canActivate(); expect(result).toEqual(true); }); it('should return organization url for redirection when updating user is not allowed', () => { let result: boolean | UrlTree; - spyOn(b2bUserService, 'isUpdatingUserAllowed').and.returnValue(false); + vi.spyOn(b2bUserService, 'isUpdatingUserAllowed').mockReturnValue(false); result = guard.canActivate(); expect(result.toString()).toBe('/organization'); expect(globalMessageService.add).toHaveBeenCalledWith( diff --git a/feature-libs/organization/administration/core/http-interceptors/bad-request/bad-request.handler.spec.ts b/feature-libs/organization/administration/core/http-interceptors/bad-request/bad-request.handler.spec.ts index 411b66def1d..715ece6211d 100644 --- a/feature-libs/organization/administration/core/http-interceptors/bad-request/bad-request.handler.spec.ts +++ b/feature-libs/organization/administration/core/http-interceptors/bad-request/bad-request.handler.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpRequest } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { @@ -87,7 +88,7 @@ describe('OrganizationBadRequestHandler', () => { }); it('should handle unit conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockUnitConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -100,7 +101,7 @@ describe('OrganizationBadRequestHandler', () => { }); it('should handle cost center conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockCostCenterConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -113,7 +114,7 @@ describe('OrganizationBadRequestHandler', () => { }); it('should handle permission conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockPermissionConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -126,7 +127,7 @@ describe('OrganizationBadRequestHandler', () => { }); it('should handle unknown conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockUnknownConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -139,7 +140,7 @@ describe('OrganizationBadRequestHandler', () => { }); it('should not handle conflict if error response does not have enough info', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, { error: {}, } as HttpErrorResponse); diff --git a/feature-libs/organization/administration/core/http-interceptors/conflict/conflict.handler.spec.ts b/feature-libs/organization/administration/core/http-interceptors/conflict/conflict.handler.spec.ts index 406007f5625..0b0bdbf4d84 100644 --- a/feature-libs/organization/administration/core/http-interceptors/conflict/conflict.handler.spec.ts +++ b/feature-libs/organization/administration/core/http-interceptors/conflict/conflict.handler.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpRequest } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { @@ -103,7 +104,7 @@ describe('OrganizationConflictHandler', () => { }); it('should handle budget conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockBudgetConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -116,7 +117,7 @@ describe('OrganizationConflictHandler', () => { }); it('should handle user conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockUpdateUserRequest, MockUserConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -129,7 +130,7 @@ describe('OrganizationConflictHandler', () => { }); it('should handle user group conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError( MockUpdateUserGroupRequest, MockUserGroupConflictResponse @@ -145,7 +146,7 @@ describe('OrganizationConflictHandler', () => { }); it('should handle unit conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockUnitConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -158,7 +159,7 @@ describe('OrganizationConflictHandler', () => { }); it('should handle cost center conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockCostCenterConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -171,7 +172,7 @@ describe('OrganizationConflictHandler', () => { }); it('should not handle conflict if error response does not have enough info', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, { error: {}, } as HttpErrorResponse); diff --git a/feature-libs/organization/administration/core/services/b2b-user.service.spec.ts b/feature-libs/organization/administration/core/services/b2b-user.service.spec.ts index d2a8c4eca89..02bb2c62c2c 100644 --- a/feature-libs/organization/administration/core/services/b2b-user.service.spec.ts +++ b/feature-libs/organization/administration/core/services/b2b-user.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { inject, TestBed } from '@angular/core/testing'; import { ofType } from '@ngrx/effects'; import { ActionsSubject, Store, StoreModule } from '@ngrx/store'; import { @@ -104,8 +105,8 @@ describe('B2BUserService', () => { store = TestBed.inject(Store); service = TestBed.inject(B2BUserService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(userIdService, 'takeUserId').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(userIdService, 'takeUserId'); actions$ = TestBed.inject(ActionsSubject); takeUserId$ = new BehaviorSubject(userId); @@ -141,8 +142,9 @@ describe('B2BUserService', () => { }); describe('get B2B user', () => { - it('get() should load B2B user when not present in the store', fakeAsync(() => { - spyOn(service, 'load').and.callThrough(); + it('get() should load B2B user when not present in the store', async () => { + vi.useFakeTimers(); + vi.spyOn(service, 'load'); const sub = service.get(orgCustomerId).subscribe(); actions$ @@ -153,10 +155,11 @@ describe('B2BUserService', () => { ); }); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(service.load).toHaveBeenCalledWith(orgCustomerId); sub.unsubscribe(); - })); + }); it('get() should be able to get user when present in the store', () => { store.dispatch( @@ -608,13 +611,13 @@ describe('B2BUserService', () => { describe('getErrorState', () => { it('getErrorState() should be able to get status error', () => { let errorState: boolean; - spyOn(service, 'getB2BUserState').and.returnValue( + vi.spyOn(service, 'getB2BUserState').mockReturnValue( of({ loading: false, success: false, error: true }) ); service.getErrorState('code').subscribe((error) => (errorState = error)); - expect(errorState).toBeTrue(); + expect(errorState).toBe(true); }); }); }); diff --git a/feature-libs/organization/administration/core/services/budget.service.spec.ts b/feature-libs/organization/administration/core/services/budget.service.spec.ts index af66c375874..ba0c1683570 100644 --- a/feature-libs/organization/administration/core/services/budget.service.spec.ts +++ b/feature-libs/organization/administration/core/services/budget.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { inject, TestBed } from '@angular/core/testing'; import { ofType } from '@ngrx/effects'; import { ActionsSubject, Store, StoreModule } from '@ngrx/store'; import { EntitiesModel, SearchConfig, UserIdService } from '@spartacus/core'; @@ -58,8 +59,8 @@ describe('BudgetService', () => { store = TestBed.inject(Store); service = TestBed.inject(BudgetService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(userIdService, 'takeUserId').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(userIdService, 'takeUserId'); takeUserId$ = new BehaviorSubject(userId); actions$ = TestBed.inject(ActionsSubject); @@ -73,8 +74,9 @@ describe('BudgetService', () => { )); describe('get budget', () => { - it('get() should trigger load budget details when they are not present in the store', fakeAsync(() => { - spyOn(service, 'loadBudget').and.callThrough(); + it('get() should trigger load budget details when they are not present in the store', async () => { + vi.useFakeTimers(); + vi.spyOn(service, 'loadBudget'); const sub = service.get(budgetCode).subscribe(); actions$ @@ -85,9 +87,10 @@ describe('BudgetService', () => { ); sub.unsubscribe(); }); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(service.loadBudget).toHaveBeenCalledWith(budgetCode); - })); + }); it('get() should be able to get budget details when they are present in the store', () => { store.dispatch(new BudgetActions.LoadBudgetSuccess([budget, budget2])); @@ -211,13 +214,13 @@ describe('BudgetService', () => { describe('getErrorState', () => { it('getErrorState() should be able to get status error', () => { let errorState: boolean; - spyOn(service, 'getBudgetState').and.returnValue( + vi.spyOn(service, 'getBudgetState').mockReturnValue( of({ loading: false, success: false, error: true }) ); service.getErrorState('code').subscribe((error) => (errorState = error)); - expect(errorState).toBeTrue(); + expect(errorState).toBe(true); }); }); }); diff --git a/feature-libs/organization/administration/core/services/cost-center.service.spec.ts b/feature-libs/organization/administration/core/services/cost-center.service.spec.ts index eb9b601feb4..49bca992194 100644 --- a/feature-libs/organization/administration/core/services/cost-center.service.spec.ts +++ b/feature-libs/organization/administration/core/services/cost-center.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { inject, TestBed } from '@angular/core/testing'; import { ofType } from '@ngrx/effects'; import { ActionsSubject, Store, StoreModule } from '@ngrx/store'; import { @@ -71,8 +72,8 @@ describe('CostCenterService', () => { store = TestBed.inject(Store); service = TestBed.inject(CostCenterService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(userIdService, 'takeUserId').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(userIdService, 'takeUserId'); actions$ = TestBed.inject(ActionsSubject); takeUserId$ = new BehaviorSubject(userId); @@ -86,8 +87,9 @@ describe('CostCenterService', () => { )); describe('get costCenter', () => { - it('get() should trigger load costCenter details when they are not present in the store', fakeAsync(() => { - spyOn(service, 'load').and.callThrough(); + it('get() should trigger load costCenter details when they are not present in the store', async () => { + vi.useFakeTimers(); + vi.spyOn(service, 'load'); const sub = service.get(costCenterCode).subscribe(); actions$ @@ -98,10 +100,11 @@ describe('CostCenterService', () => { ); }); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(service.load).toHaveBeenCalledWith(costCenterCode); sub.unsubscribe(); - })); + }); it('get() should be able to get costCenter details when they are present in the store', () => { store.dispatch( @@ -322,13 +325,13 @@ describe('CostCenterService', () => { describe('getErrorState', () => { it('getErrorState() should be able to get status error', () => { let errorState: boolean; - spyOn(service, 'getCostCenterState').and.returnValue( + vi.spyOn(service, 'getCostCenterState').mockReturnValue( of({ loading: false, success: false, error: true }) ); service.getErrorState('code').subscribe((error) => (errorState = error)); - expect(errorState).toBeTrue(); + expect(errorState).toBe(true); }); }); }); diff --git a/feature-libs/organization/administration/core/services/org-unit.service.spec.ts b/feature-libs/organization/administration/core/services/org-unit.service.spec.ts index c583deb334b..3ff846dc8a7 100644 --- a/feature-libs/organization/administration/core/services/org-unit.service.spec.ts +++ b/feature-libs/organization/administration/core/services/org-unit.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { inject, TestBed } from '@angular/core/testing'; import { ofType } from '@ngrx/effects'; import { ActionsSubject, Store, StoreModule } from '@ngrx/store'; import { @@ -128,8 +129,8 @@ describe('OrgUnitService', () => { store = TestBed.inject(Store); service = TestBed.inject(OrgUnitService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(userIdService, 'takeUserId').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(userIdService, 'takeUserId'); actions$ = TestBed.inject(ActionsSubject); takeUserId$ = new BehaviorSubject(userId); @@ -143,8 +144,9 @@ describe('OrgUnitService', () => { )); describe('get orgUnit', () => { - it('get() should trigger load orgUnit details when they are not present in the store', fakeAsync(() => { - spyOn(service, 'load').and.callThrough(); + it('get() should trigger load orgUnit details when they are not present in the store', async () => { + vi.useFakeTimers(); + vi.spyOn(service, 'load'); const sub = service.get(orgUnitId).subscribe(); actions$ @@ -155,10 +157,11 @@ describe('OrgUnitService', () => { ); }); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(service.load).toHaveBeenCalledWith(orgUnitId); sub.unsubscribe(); - })); + }); it('get() should be able to get orgUnit details when they are present in the store', () => { store.dispatch(new OrgUnitActions.LoadOrgUnitSuccess([orgUnit])); @@ -769,13 +772,13 @@ describe('OrgUnitService', () => { describe('getErrorState', () => { it('getErrorState() should be able to get status error', () => { let errorState: boolean; - spyOn(service, 'getOrgUnitState').and.returnValue( + vi.spyOn(service, 'getOrgUnitState').mockReturnValue( of({ loading: false, success: false, error: true }) ); service.getErrorState('code').subscribe((error) => (errorState = error)); - expect(errorState).toBeTrue(); + expect(errorState).toBe(true); }); }); }); diff --git a/feature-libs/organization/administration/core/services/organization-page-meta.resolver.spec.ts b/feature-libs/organization/administration/core/services/organization-page-meta.resolver.spec.ts index eb57fda678a..1496985e987 100644 --- a/feature-libs/organization/administration/core/services/organization-page-meta.resolver.spec.ts +++ b/feature-libs/organization/administration/core/services/organization-page-meta.resolver.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { BreadcrumbMeta, @@ -25,7 +26,7 @@ const organizationBreadcrumb: BreadcrumbMeta = { }; class MockSemanticPathService implements Partial { - get = jasmine.createSpy('get').and.returnValue(testOrganizationUrl); + get = vi.fn().mockReturnValue(testOrganizationUrl); } const testHomeBreadcrumb: BreadcrumbMeta = { label: 'Test Home', link: '/' }; @@ -82,7 +83,7 @@ describe('OrganizationPageMetaResolver', () => { describe('resolveBreadcrumbs', () => { describe('when being on the Organization page', () => { beforeEach(() => { - spyOn(routingService, 'getRouterState').and.returnValue( + vi.spyOn(routingService, 'getRouterState').mockReturnValue( of({ state: { semanticRoute: 'organization' } } as any) ); }); @@ -101,11 +102,11 @@ describe('OrganizationPageMetaResolver', () => { }; beforeEach(() => { - spyOn(routingService, 'getRouterState').and.returnValue( + vi.spyOn(routingService, 'getRouterState').mockReturnValue( of({ state: { semanticRoute: 'orgBudgetDetails' } } as any) ); - spyOn(contentPageMetaResolver, 'resolveBreadcrumbs').and.returnValue( + vi.spyOn(contentPageMetaResolver, 'resolveBreadcrumbs').mockReturnValue( of([testHomeBreadcrumb, testBudgetsBreadcrumb]) ); }); diff --git a/feature-libs/organization/administration/core/services/permission.service.spec.ts b/feature-libs/organization/administration/core/services/permission.service.spec.ts index df6c9e96098..946d674d1c9 100644 --- a/feature-libs/organization/administration/core/services/permission.service.spec.ts +++ b/feature-libs/organization/administration/core/services/permission.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { inject, TestBed } from '@angular/core/testing'; import { ofType } from '@ngrx/effects'; import { ActionsSubject, Store, StoreModule } from '@ngrx/store'; import { @@ -68,8 +69,8 @@ describe('PermissionService', () => { store = TestBed.inject(Store); service = TestBed.inject(PermissionService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(userIdService, 'takeUserId').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(userIdService, 'takeUserId'); actions$ = TestBed.inject(ActionsSubject); takeUserId$ = new BehaviorSubject(userId); @@ -83,8 +84,9 @@ describe('PermissionService', () => { )); describe('get permission', () => { - it('get() should trigger load permission details when they are not present in the store', fakeAsync(() => { - spyOn(service, 'loadPermission').and.callThrough(); + it('get() should trigger load permission details when they are not present in the store', async () => { + vi.useFakeTimers(); + vi.spyOn(service, 'loadPermission'); const sub = service.get(permissionCode).subscribe(); actions$ @@ -95,10 +97,11 @@ describe('PermissionService', () => { ); }); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(service.loadPermission).toHaveBeenCalledWith(permissionCode); sub.unsubscribe(); - })); + }); it('get() should be able to get permission details when they are present in the store', () => { store.dispatch( @@ -272,13 +275,13 @@ describe('PermissionService', () => { describe('getErrorState', () => { it('getErrorState() should be able to get status error', () => { let errorState: boolean; - spyOn(service, 'getPermissionState').and.returnValue( + vi.spyOn(service, 'getPermissionState').mockReturnValue( of({ loading: false, success: false, error: true }) ); service.getErrorState('code').subscribe((error) => (errorState = error)); - expect(errorState).toBeTrue(); + expect(errorState).toBe(true); }); }); }); diff --git a/feature-libs/organization/administration/core/services/user-group.service.spec.ts b/feature-libs/organization/administration/core/services/user-group.service.spec.ts index 7523aafe135..34e0650a478 100644 --- a/feature-libs/organization/administration/core/services/user-group.service.spec.ts +++ b/feature-libs/organization/administration/core/services/user-group.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { inject, TestBed } from '@angular/core/testing'; import { ofType } from '@ngrx/effects'; import { ActionsSubject, Store, StoreModule } from '@ngrx/store'; import { @@ -100,8 +101,8 @@ describe('UserGroupService', () => { store = TestBed.inject(Store); service = TestBed.inject(UserGroupService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(userIdService, 'takeUserId').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(userIdService, 'takeUserId'); actions$ = TestBed.inject(ActionsSubject); takeUserId$ = new BehaviorSubject(userId); @@ -115,8 +116,9 @@ describe('UserGroupService', () => { )); describe('get userGroup', () => { - it('get() should trigger load userGroup details when they are not present in the store', fakeAsync(() => { - spyOn(service, 'load').and.callThrough(); + it('get() should trigger load userGroup details when they are not present in the store', async () => { + vi.useFakeTimers(); + vi.spyOn(service, 'load'); const sub = service.get(userGroupId).subscribe(); actions$ @@ -130,10 +132,11 @@ describe('UserGroupService', () => { ); }); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(service.load).toHaveBeenCalledWith(userGroupId); sub.unsubscribe(); - })); + }); it('get() should be able to get userGroup details when they are present in the store', () => { store.dispatch( @@ -472,13 +475,13 @@ describe('UserGroupService', () => { describe('getErrorState', () => { it('getErrorState() should be able to get status error', () => { let errorState: boolean; - spyOn(service, 'getUserGroupState').and.returnValue( + vi.spyOn(service, 'getUserGroupState').mockReturnValue( of({ loading: false, success: false, error: true }) ); service.getErrorState('code').subscribe((error) => (errorState = error)); - expect(errorState).toBeTrue(); + expect(errorState).toBe(true); }); }); }); diff --git a/feature-libs/organization/administration/core/store/effects/b2b-user.effect.spec.ts b/feature-libs/organization/administration/core/store/effects/b2b-user.effect.spec.ts index e27da582beb..6298d05a655 100644 --- a/feature-libs/organization/administration/core/store/effects/b2b-user.effect.spec.ts +++ b/feature-libs/organization/administration/core/store/effects/b2b-user.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -34,7 +35,6 @@ import { UserGroupActions, } from '../actions/index'; import * as fromEffects from './b2b-user.effect'; -import createSpy = jasmine.createSpy; const httpErrorResponse = new HttpErrorResponse({ error: 'error', @@ -99,52 +99,50 @@ class MockLoggerService { } class MockRoutingService { - go = createSpy('go').and.stub(); - getRouterState = createSpy('getRouterState').and.returnValue( - of(mockRouterState) - ); + go = vi.fn().mockImplementation(() => {}); + getRouterState = vi.fn().mockReturnValue(of(mockRouterState)); } class MockB2BUserConnector { - get = createSpy().and.returnValue(of(orgCustomer)); - getList = createSpy().and.returnValue( - of({ values: [orgCustomer], pagination, sorts }) - ); - getUserGroups = createSpy().and.returnValue( - of({ values: [userGroup], pagination, sorts }) - ); - getApprovers = createSpy().and.returnValue( - of({ values: [orgCustomer], pagination, sorts }) - ); - getPermissions = createSpy().and.returnValue( - of({ values: [permission], pagination, sorts }) - ); - assignApprover = createSpy().and.returnValue( - of({ id: approverId, selected: true }) - ); - unassignApprover = createSpy().and.returnValue( - of({ id: approverId, selected: false }) - ); - assignPermission = createSpy().and.returnValue( - of({ id: permissionId, selected: true }) - ); - unassignPermission = createSpy().and.returnValue( - of({ id: permissionId, selected: false }) - ); - assignUserGroup = createSpy().and.returnValue( - of({ id: userGroupId, selected: true }) - ); - unassignUserGroup = createSpy().and.returnValue( - of({ id: userGroupId, selected: false }) - ); - create = createSpy().and.returnValue(of(orgCustomer)); - update = createSpy().and.returnValue(of(orgCustomer)); + get = vi.fn().mockReturnValue(of(orgCustomer)); + getList = vi + .fn() + .mockReturnValue(of({ values: [orgCustomer], pagination, sorts })); + getUserGroups = vi + .fn() + .mockReturnValue(of({ values: [userGroup], pagination, sorts })); + getApprovers = vi + .fn() + .mockReturnValue(of({ values: [orgCustomer], pagination, sorts })); + getPermissions = vi + .fn() + .mockReturnValue(of({ values: [permission], pagination, sorts })); + assignApprover = vi + .fn() + .mockReturnValue(of({ id: approverId, selected: true })); + unassignApprover = vi + .fn() + .mockReturnValue(of({ id: approverId, selected: false })); + assignPermission = vi + .fn() + .mockReturnValue(of({ id: permissionId, selected: true })); + unassignPermission = vi + .fn() + .mockReturnValue(of({ id: permissionId, selected: false })); + assignUserGroup = vi + .fn() + .mockReturnValue(of({ id: userGroupId, selected: true })); + unassignUserGroup = vi + .fn() + .mockReturnValue(of({ id: userGroupId, selected: false })); + create = vi.fn().mockReturnValue(of(orgCustomer)); + update = vi.fn().mockReturnValue(of(orgCustomer)); } class MockUserAccountFacade implements Partial { - get = createSpy().and.returnValue(of(mockCurrentUser)); + get = vi.fn().mockReturnValue(of(mockCurrentUser)); } class MockUserIdService implements Partial { - getUserId = createSpy().and.returnValue(of('current')); + getUserId = vi.fn().mockReturnValue(of('current')); } const error = tryNormalizeHttpError(httpErrorResponse, new MockLoggerService()); @@ -209,9 +207,9 @@ describe('B2B User Effects', () => { }); it('should return LoadB2BUserFail action if user not loaded', () => { - b2bUserConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.LoadB2BUser({ userId, orgCustomerId }); const completion = new B2BUserActions.LoadB2BUserFail({ orgCustomerId, @@ -241,9 +239,9 @@ describe('B2B User Effects', () => { }); it('should return LoadB2BUsersFail action if B2B Users not loaded', () => { - b2bUserConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.LoadB2BUsers({ userId, params }); const completion = new B2BUserActions.LoadB2BUsersFail({ error, params }); actions$ = hot('-a', { a: action }); @@ -279,9 +277,9 @@ describe('B2B User Effects', () => { }); it('should return LoadB2BUserUserGroupsFail action if B2BUser UserGroup not loaded', () => { - b2bUserConnector.getUserGroups = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.getUserGroups = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.LoadB2BUserUserGroups({ userId, orgCustomerId, @@ -325,9 +323,9 @@ describe('B2B User Effects', () => { }); it('should return CreateB2BUserFail action if user not created', () => { - b2bUserConnector.create = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.create = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.CreateB2BUser({ userId, orgCustomer }); const completion1 = new B2BUserActions.CreateB2BUserFail({ orgCustomerId, @@ -364,9 +362,9 @@ describe('B2B User Effects', () => { }); it('should return UpdateB2BUserFail action if user not updated', () => { - b2bUserConnector.update = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.update = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.UpdateB2BUser({ userId, orgCustomerId, @@ -435,9 +433,9 @@ describe('B2B User Effects', () => { }); it('should return LoadB2BUserApproversFail action if approvers not loaded', () => { - b2bUserConnector.getApprovers = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.getApprovers = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.LoadB2BUserApprovers({ userId, orgCustomerId, @@ -487,9 +485,9 @@ describe('B2B User Effects', () => { }); it('should return LoadB2BUserApproversFail action if Permissions not loaded', () => { - b2bUserConnector.getPermissions = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.getPermissions = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.LoadB2BUserPermissions({ userId, orgCustomerId, @@ -535,9 +533,9 @@ describe('B2B User Effects', () => { }); it('should return AssignB2BUserApproverFail action if approver not assigned', () => { - b2bUserConnector.assignApprover = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.assignApprover = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.AssignB2BUserApprover({ userId, orgCustomerId, @@ -584,9 +582,9 @@ describe('B2B User Effects', () => { }); it('should return UnassignB2BUserApproverFail action if approver not unassigned', () => { - b2bUserConnector.unassignApprover = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.unassignApprover = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.UnassignB2BUserApprover({ userId, orgCustomerId, @@ -634,9 +632,9 @@ describe('B2B User Effects', () => { }); it('should return AssignB2BUserPermissionFail action if permission not assigned', () => { - b2bUserConnector.assignPermission = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.assignPermission = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.AssignB2BUserPermission({ userId, orgCustomerId, @@ -684,9 +682,9 @@ describe('B2B User Effects', () => { }); it('should return UnassignB2BUserPermissionFail action if permission not unassigned', () => { - b2bUserConnector.unassignPermission = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.unassignPermission = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.UnassignB2BUserPermission({ userId, orgCustomerId, @@ -734,9 +732,9 @@ describe('B2B User Effects', () => { }); it('should return AssignB2BUserUserGroupFail action if UserGroup was not assigned', () => { - b2bUserConnector.assignUserGroup = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.assignUserGroup = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.AssignB2BUserUserGroup({ userId, orgCustomerId, @@ -784,9 +782,9 @@ describe('B2B User Effects', () => { }); it('should return UnassignB2BUserUserGroupFail action if UserGroup was not unassigned', () => { - b2bUserConnector.unassignUserGroup = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + b2bUserConnector.unassignUserGroup = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new B2BUserActions.UnassignB2BUserUserGroup({ userId, orgCustomerId, diff --git a/feature-libs/organization/administration/core/store/effects/budget.effect.spec.ts b/feature-libs/organization/administration/core/store/effects/budget.effect.spec.ts index bd82b34e3fc..e57c3401ae8 100644 --- a/feature-libs/organization/administration/core/store/effects/budget.effect.spec.ts +++ b/feature-libs/organization/administration/core/store/effects/budget.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -25,8 +26,6 @@ import { Observable, of, throwError } from 'rxjs'; import { BudgetActions } from '../actions/index'; import * as fromEffects from './budget.effect'; -import createSpy = jasmine.createSpy; - const httpErrorResponse = new HttpErrorResponse({ error: 'error', headers: new HttpHeaders().set('xxx', 'xxx'), @@ -52,12 +51,12 @@ const pagination = { currentPage: 1 }; const sorts = [{ selected: true, name: 'code' }]; class MockBudgetConnector { - get = createSpy().and.returnValue(of(budget)); - getList = createSpy().and.returnValue( - of({ values: [budget], pagination, sorts }) - ); - create = createSpy().and.returnValue(of(budget)); - update = createSpy().and.returnValue(of(budget)); + get = vi.fn().mockReturnValue(of(budget)); + getList = vi + .fn() + .mockReturnValue(of({ values: [budget], pagination, sorts })); + create = vi.fn().mockReturnValue(of(budget)); + update = vi.fn().mockReturnValue(of(budget)); } class MockLoggerService { log(): void {} @@ -124,9 +123,9 @@ describe('Budget Effects', () => { }); it('should return LoadBudgetFail action if budget not updated', () => { - budgetConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + budgetConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new BudgetActions.LoadBudget({ userId, budgetCode }); const completion = new BudgetActions.LoadBudgetFail({ budgetCode, @@ -158,9 +157,9 @@ describe('Budget Effects', () => { }); it('should return LoadBudgetsFail action if budgets not loaded', () => { - budgetConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + budgetConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new BudgetActions.LoadBudgets({ userId, params }); const completion = new BudgetActions.LoadBudgetsFail({ error, params }); actions$ = hot('-a', { a: action }); @@ -184,9 +183,9 @@ describe('Budget Effects', () => { }); it('should return CreateBudgetFail action if budget not created', () => { - budgetConnector.create = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + budgetConnector.create = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new BudgetActions.CreateBudget({ userId, budget }); const completion1 = new BudgetActions.CreateBudgetFail({ budgetCode, @@ -222,9 +221,9 @@ describe('Budget Effects', () => { }); it('should return UpdateBudgetFail action if budget not created', () => { - budgetConnector.update = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + budgetConnector.update = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new BudgetActions.UpdateBudget({ userId, budgetCode, diff --git a/feature-libs/organization/administration/core/store/effects/cost-center.effect.spec.ts b/feature-libs/organization/administration/core/store/effects/cost-center.effect.spec.ts index a1a828f9f4c..7d7cebbeaee 100644 --- a/feature-libs/organization/administration/core/store/effects/cost-center.effect.spec.ts +++ b/feature-libs/organization/administration/core/store/effects/cost-center.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -26,8 +27,6 @@ import { Observable, of, throwError } from 'rxjs'; import { BudgetActions, CostCenterActions } from '../actions/index'; import * as fromEffects from './cost-center.effect'; -import createSpy = jasmine.createSpy; - const httpErrorResponse = new HttpErrorResponse({ error: 'error', headers: new HttpHeaders().set('xxx', 'xxx'), @@ -62,17 +61,17 @@ const pagination = { currentPage: 1 }; const sorts = [{ selected: true, name: 'code' }]; class MockCostCenterConnector implements Partial { - get = createSpy().and.returnValue(of(costCenter)); - getList = createSpy().and.returnValue( - of({ values: [costCenter], pagination, sorts }) - ); - create = createSpy().and.returnValue(of(costCenter)); - update = createSpy().and.returnValue(of(costCenter)); - getBudgets = createSpy().and.returnValue( - of({ values: [budget], pagination, sorts }) - ); - assignBudget = createSpy().and.returnValue(of(null)); - unassignBudget = createSpy().and.returnValue(of(null)); + get = vi.fn().mockReturnValue(of(costCenter)); + getList = vi + .fn() + .mockReturnValue(of({ values: [costCenter], pagination, sorts })); + create = vi.fn().mockReturnValue(of(costCenter)); + update = vi.fn().mockReturnValue(of(costCenter)); + getBudgets = vi + .fn() + .mockReturnValue(of({ values: [budget], pagination, sorts })); + assignBudget = vi.fn().mockReturnValue(of(null)); + unassignBudget = vi.fn().mockReturnValue(of(null)); } class MockLoggerService { @@ -148,9 +147,9 @@ describe('CostCenter Effects', () => { }); it('should return LoadCostCenterFail action if costCenter not updated', () => { - costCenterConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.LoadCostCenter({ userId, costCenterCode, @@ -190,9 +189,9 @@ describe('CostCenter Effects', () => { }); it('should return LoadCostCentersFail action if costCenters not loaded', () => { - costCenterConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.LoadCostCenters({ userId, params }); const completion = new CostCenterActions.LoadCostCentersFail({ error, @@ -227,9 +226,9 @@ describe('CostCenter Effects', () => { }); it('should return CreateCostCenterFail action if costCenter not created', () => { - costCenterConnector.create = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.create = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.CreateCostCenter({ userId, costCenter, @@ -273,9 +272,9 @@ describe('CostCenter Effects', () => { }); it('should return UpdateCostCenterFail action if costCenter not created', () => { - costCenterConnector.update = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.update = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.UpdateCostCenter({ userId, costCenterCode, @@ -325,9 +324,9 @@ describe('CostCenter Effects', () => { }); it('should return LoadAssignedBudgetsFail action if budgets not loaded', () => { - costCenterConnector.getBudgets = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.getBudgets = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.LoadAssignedBudgets({ userId, costCenterCode, @@ -374,9 +373,9 @@ describe('CostCenter Effects', () => { }); it('should return UpdateCostCenterFail action if budget not assigned', () => { - costCenterConnector.assignBudget = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.assignBudget = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.AssignBudget({ userId, costCenterCode, @@ -422,9 +421,9 @@ describe('CostCenter Effects', () => { }); it('should return UnassignBudgetFail action if budget not unassigned', () => { - costCenterConnector.unassignBudget = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + costCenterConnector.unassignBudget = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new CostCenterActions.UnassignBudget({ userId, costCenterCode, diff --git a/feature-libs/organization/administration/core/store/effects/org-unit.effect.spec.ts b/feature-libs/organization/administration/core/store/effects/org-unit.effect.spec.ts index 9f59753a6c6..1624e743799 100644 --- a/feature-libs/organization/administration/core/store/effects/org-unit.effect.spec.ts +++ b/feature-libs/organization/administration/core/store/effects/org-unit.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -31,8 +32,6 @@ import { B2BUnitNode } from '../../model/unit-node.model'; import { B2BUserActions, OrgUnitActions } from '../actions/index'; import * as fromEffects from './org-unit.effect'; -import createSpy = jasmine.createSpy; - const httpErrorResponse = new HttpErrorResponse({ error: 'error', headers: new HttpHeaders().set('xxx', 'xxx'), @@ -70,20 +69,20 @@ const users: EntitiesModel = { }; class MockOrgUnitConnector { - get = createSpy().and.returnValue(of(orgUnit)); - getList = createSpy().and.returnValue(of(orgUnitList)); - create = createSpy().and.returnValue(of(orgUnit)); - update = createSpy().and.returnValue(of(orgUnit)); - createAddress = createSpy().and.returnValue(of(address)); - updateAddress = createSpy().and.returnValue(of(address)); - deleteAddress = createSpy().and.returnValue(of(address)); - assignRole = createSpy().and.returnValue(of(roleId)); - unassignRole = createSpy().and.returnValue(of(roleId)); - assignApprover = createSpy().and.returnValue(of(roleId)); - unassignApprover = createSpy().and.returnValue(of(roleId)); - getApprovalProcesses = createSpy().and.returnValue(of(approvalProcesses)); - getUsers = createSpy().and.returnValue(of(users)); - getTree = createSpy().and.returnValue(of(unitNode)); + get = vi.fn().mockReturnValue(of(orgUnit)); + getList = vi.fn().mockReturnValue(of(orgUnitList)); + create = vi.fn().mockReturnValue(of(orgUnit)); + update = vi.fn().mockReturnValue(of(orgUnit)); + createAddress = vi.fn().mockReturnValue(of(address)); + updateAddress = vi.fn().mockReturnValue(of(address)); + deleteAddress = vi.fn().mockReturnValue(of(address)); + assignRole = vi.fn().mockReturnValue(of(roleId)); + unassignRole = vi.fn().mockReturnValue(of(roleId)); + assignApprover = vi.fn().mockReturnValue(of(roleId)); + unassignApprover = vi.fn().mockReturnValue(of(roleId)); + getApprovalProcesses = vi.fn().mockReturnValue(of(approvalProcesses)); + getUsers = vi.fn().mockReturnValue(of(users)); + getTree = vi.fn().mockReturnValue(of(unitNode)); } class MockLoggerService { @@ -141,7 +140,7 @@ describe('OrgUnit Effects', () => { describe('load$', () => { // TODO: unlock after use final addresses endpoint - xit('should return LoadOrgUnitSuccess action', () => { + it.skip('should return LoadOrgUnitSuccess action', () => { const action = new OrgUnitActions.LoadOrgUnit({ userId, orgUnitId }); const completion = new OrgUnitActions.LoadOrgUnitSuccess([orgUnit]); actions$ = hot('-a', { a: action }); @@ -152,9 +151,9 @@ describe('OrgUnit Effects', () => { }); it('should return LoadOrgUnitFail action if orgUnit not updated', () => { - orgUnitConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.LoadOrgUnit({ userId, orgUnitId }); const completion = new OrgUnitActions.LoadOrgUnitFail({ orgUnitId, @@ -182,9 +181,9 @@ describe('OrgUnit Effects', () => { }); it('should return LoadOrgUnitNodesFail action if orgUnits not loaded', () => { - orgUnitConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.LoadOrgUnitNodes({ userId }); const completion = new OrgUnitActions.LoadOrgUnitNodesFail({ error }); actions$ = hot('-a', { a: action }); @@ -208,9 +207,9 @@ describe('OrgUnit Effects', () => { }); it('should return LoadOrgUnitNodesFail action if orgUnits not loaded', () => { - orgUnitConnector.create = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.create = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.CreateUnit({ userId, unit: orgUnit }); const completion1 = new OrgUnitActions.CreateUnitFail({ unitCode: orgUnitId, @@ -227,7 +226,7 @@ describe('OrgUnit Effects', () => { describe('updateUnit$', () => { // TODO: unlock after get correct response and fixed effect - xit('should return UpdateOrgUnitNodesSuccess action', () => { + it.skip('should return UpdateOrgUnitNodesSuccess action', () => { const action = new OrgUnitActions.UpdateUnit({ userId, unitCode: orgUnitId, @@ -246,9 +245,9 @@ describe('OrgUnit Effects', () => { }); it('should return UpdateOrgUnitNodesFail action if orgUnits not loaded', () => { - orgUnitConnector.update = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.update = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.UpdateUnit({ userId, unitCode: orgUnitId, @@ -299,9 +298,9 @@ describe('OrgUnit Effects', () => { }); it('should return CreateAddressFail action if address is not loaded', () => { - orgUnitConnector.createAddress = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.createAddress = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.CreateAddress({ userId, orgUnitId, @@ -349,9 +348,9 @@ describe('OrgUnit Effects', () => { }); it('should return UpdateAddressFail action if address is not loaded', () => { - orgUnitConnector.updateAddress = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.updateAddress = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.UpdateAddress({ userId, orgUnitId, @@ -397,9 +396,9 @@ describe('OrgUnit Effects', () => { }); it('should return DeleteAddressFail action if address is not loaded', () => { - orgUnitConnector.deleteAddress = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.deleteAddress = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.DeleteAddress({ userId, orgUnitId, @@ -446,9 +445,9 @@ describe('OrgUnit Effects', () => { }); it('should return AssignRoleFail action if address is not loaded', () => { - orgUnitConnector.assignRole = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.assignRole = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.AssignRole({ userId, orgCustomerId, @@ -494,9 +493,9 @@ describe('OrgUnit Effects', () => { }); it('should return UnassignRoleFail action if address is not loaded', () => { - orgUnitConnector.unassignRole = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.unassignRole = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.UnassignRole({ userId, orgCustomerId, @@ -545,9 +544,9 @@ describe('OrgUnit Effects', () => { }); it('should return AssignApproverFail action if address is not loaded', () => { - orgUnitConnector.assignApprover = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.assignApprover = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.AssignApprover({ userId, orgUnitId, @@ -599,9 +598,9 @@ describe('OrgUnit Effects', () => { }); it('should return UnassignApproverFail action if address is not loaded', () => { - orgUnitConnector.unassignApprover = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.unassignApprover = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.UnassignApprover({ userId, orgUnitId, @@ -644,9 +643,9 @@ describe('OrgUnit Effects', () => { }); it('should return LoadApprovalProcessesFail action if address is not loaded', () => { - orgUnitConnector.getApprovalProcesses = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.getApprovalProcesses = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.LoadApprovalProcesses({ userId, }); @@ -698,9 +697,9 @@ describe('OrgUnit Effects', () => { }); it('should return LoadUsersFail action if address is not loaded', () => { - orgUnitConnector.getUsers = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.getUsers = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.LoadAssignedUsers({ userId, orgUnitId, @@ -740,9 +739,9 @@ describe('OrgUnit Effects', () => { }); it('should return LoadTreeFail action if address is not loaded', () => { - orgUnitConnector.getTree = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orgUnitConnector.getTree = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrgUnitActions.LoadTree({ userId, }); diff --git a/feature-libs/organization/administration/core/store/effects/permission.effect.spec.ts b/feature-libs/organization/administration/core/store/effects/permission.effect.spec.ts index 7db8bccce77..e83056ceecf 100644 --- a/feature-libs/organization/administration/core/store/effects/permission.effect.spec.ts +++ b/feature-libs/organization/administration/core/store/effects/permission.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -25,7 +26,6 @@ import { Observable, of, throwError } from 'rxjs'; import { Permission } from '../../model/permission.model'; import { PermissionActions } from '../actions/index'; import * as fromEffects from './permission.effect'; -import createSpy = jasmine.createSpy; const httpErrorResponse = new HttpErrorResponse({ error: 'error', @@ -54,13 +54,13 @@ const pagination = { currentPage: 1 }; const sorts = [{ selected: true, name: 'code' }]; class MockPermissionConnector { - get = createSpy().and.returnValue(of(permission)); - getList = createSpy().and.returnValue( - of({ values: [permission], pagination, sorts }) - ); - create = createSpy().and.returnValue(of(permission)); - update = createSpy().and.returnValue(of(permission)); - getTypes = createSpy().and.returnValue(of(permissionTypes)); + get = vi.fn().mockReturnValue(of(permission)); + getList = vi + .fn() + .mockReturnValue(of({ values: [permission], pagination, sorts })); + create = vi.fn().mockReturnValue(of(permission)); + update = vi.fn().mockReturnValue(of(permission)); + getTypes = vi.fn().mockReturnValue(of(permissionTypes)); } class MockLoggerService { @@ -136,9 +136,9 @@ describe('Permission Effects', () => { }); it('should return LoadPermissionFail action if permission not updated', () => { - permissionConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + permissionConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new PermissionActions.LoadPermission({ userId, permissionCode, @@ -178,9 +178,9 @@ describe('Permission Effects', () => { }); it('should return LoadPermissionsFail action if permissions not loaded', () => { - permissionConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + permissionConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new PermissionActions.LoadPermissions({ userId, params }); const completion = new PermissionActions.LoadPermissionsFail({ error, @@ -215,9 +215,9 @@ describe('Permission Effects', () => { }); it('should return CreatePermissionFail action if permission not created', () => { - permissionConnector.create = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + permissionConnector.create = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new PermissionActions.CreatePermission({ userId, permission, @@ -261,9 +261,9 @@ describe('Permission Effects', () => { }); it('should return UpdatePermissionFail action if permission not created', () => { - permissionConnector.update = createSpy('update').and.returnValue( - throwError(() => httpErrorResponse) - ); + permissionConnector.update = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new PermissionActions.UpdatePermission({ userId, permissionCode, @@ -300,9 +300,9 @@ describe('Permission Effects', () => { }); it('should return LoadPermissionTypesFail action if permission types are not updated', () => { - permissionConnector.getTypes = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + permissionConnector.getTypes = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new PermissionActions.LoadPermissionTypes(); const completion = new PermissionActions.LoadPermissionTypesFail({ error, diff --git a/feature-libs/organization/administration/core/store/effects/user-group.effect.spec.ts b/feature-libs/organization/administration/core/store/effects/user-group.effect.spec.ts index 0f6c15dfce8..21a1944eb02 100644 --- a/feature-libs/organization/administration/core/store/effects/user-group.effect.spec.ts +++ b/feature-libs/organization/administration/core/store/effects/user-group.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -29,7 +30,6 @@ import { UserGroupActions, } from '../actions'; import * as fromEffects from './user-group.effect'; -import createSpy = jasmine.createSpy; const httpErrorResponse = new HttpErrorResponse({ error: 'error', @@ -60,32 +60,32 @@ const customer = { }; class MockUserGroupConnector implements Partial { - get = createSpy().and.returnValue(of(userGroup)); - getList = createSpy().and.returnValue( - of({ values: [userGroup], pagination, sorts }) - ); - create = createSpy().and.returnValue(of(userGroup)); - update = createSpy().and.returnValue(of(userGroup)); - delete = createSpy().and.returnValue(of(userGroup)); - getAvailableOrderApprovalPermissions = createSpy().and.returnValue( - of({ values: [permission], pagination, sorts }) - ); - assignOrderApprovalPermission = createSpy().and.returnValue( - of({ id: permissionUid, selected: true }) - ); - unassignOrderApprovalPermission = createSpy().and.returnValue( - of({ id: permissionUid, selected: false }) - ); - getAvailableOrgCustomers = createSpy().and.returnValue( - of({ values: [customer], pagination, sorts }) - ); - assignMember = createSpy().and.returnValue( - of({ id: customerId, selected: true }) - ); - unassignMember = createSpy().and.returnValue( - of({ id: customerId, selected: false }) - ); - unassignAllMembers = createSpy().and.returnValue(of(null)); + get = vi.fn().mockReturnValue(of(userGroup)); + getList = vi + .fn() + .mockReturnValue(of({ values: [userGroup], pagination, sorts })); + create = vi.fn().mockReturnValue(of(userGroup)); + update = vi.fn().mockReturnValue(of(userGroup)); + delete = vi.fn().mockReturnValue(of(userGroup)); + getAvailableOrderApprovalPermissions = vi + .fn() + .mockReturnValue(of({ values: [permission], pagination, sorts })); + assignOrderApprovalPermission = vi + .fn() + .mockReturnValue(of({ id: permissionUid, selected: true })); + unassignOrderApprovalPermission = vi + .fn() + .mockReturnValue(of({ id: permissionUid, selected: false })); + getAvailableOrgCustomers = vi + .fn() + .mockReturnValue(of({ values: [customer], pagination, sorts })); + assignMember = vi + .fn() + .mockReturnValue(of({ id: customerId, selected: true })); + unassignMember = vi + .fn() + .mockReturnValue(of({ id: customerId, selected: false })); + unassignAllMembers = vi.fn().mockReturnValue(of(null)); } class MockLoggerService { @@ -167,9 +167,9 @@ describe('UserGroup Effects', () => { }); it('should return LoadUserGroupFail action if userGroup not updated', () => { - userGroupConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.LoadUserGroup({ userId, userGroupId, @@ -207,9 +207,9 @@ describe('UserGroup Effects', () => { }); it('should return LoadUserGroupsFail action if userGroups not loaded', () => { - userGroupConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.LoadUserGroups({ userId, params, @@ -244,9 +244,9 @@ describe('UserGroup Effects', () => { }); it('should return CreateUserGroupFail action if userGroup not created', () => { - userGroupConnector.create = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.create = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.CreateUserGroup({ userId, userGroup, @@ -266,7 +266,7 @@ describe('UserGroup Effects', () => { describe('updateUserGroup$', () => { // TODO: unlock after get correct response and fixed effect - xit('should return UpdateUserGroupSuccess action', () => { + it.skip('should return UpdateUserGroupSuccess action', () => { const action = new UserGroupActions.UpdateUserGroup({ userId, userGroupId, @@ -285,9 +285,9 @@ describe('UserGroup Effects', () => { }); it('should return UpdateUserGroupFail action if userGroup not created', () => { - userGroupConnector.update = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.update = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.UpdateUserGroup({ userId, userGroupId, @@ -331,9 +331,9 @@ describe('UserGroup Effects', () => { }); it('should return DeleteUserGroupFail action if userGroup not created', () => { - userGroupConnector.delete = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.delete = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.DeleteUserGroup({ userId, userGroupId, @@ -383,8 +383,9 @@ describe('UserGroup Effects', () => { }); it('should return LoadPermissionFail action if permissions not loaded', () => { - userGroupConnector.getAvailableOrderApprovalPermissions = - createSpy().and.returnValue(throwError(() => httpErrorResponse)); + userGroupConnector.getAvailableOrderApprovalPermissions = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.LoadPermissions({ userId, userGroupId, @@ -429,8 +430,9 @@ describe('UserGroup Effects', () => { }); it('should return CreateUserGroupOrderApprovalPermissionFail action if permission not assigned', () => { - userGroupConnector.assignOrderApprovalPermission = - createSpy().and.returnValue(throwError(() => httpErrorResponse)); + userGroupConnector.assignOrderApprovalPermission = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.AssignPermission({ userId, userGroupId, @@ -473,8 +475,9 @@ describe('UserGroup Effects', () => { }); it('should return DeleteUserGroupOrderApprovalPermissionFail action if permission not unassigned', () => { - userGroupConnector.unassignOrderApprovalPermission = - createSpy().and.returnValue(throwError(() => httpErrorResponse)); + userGroupConnector.unassignOrderApprovalPermission = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.UnassignPermission({ userId, userGroupId, @@ -525,9 +528,9 @@ describe('UserGroup Effects', () => { }); it('should return LoadUserGroupAvailableOrgCustomersFail action if users not loaded', () => { - userGroupConnector.getAvailableOrgCustomers = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.getAvailableOrgCustomers = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.LoadAvailableOrgCustomers({ userId, userGroupId, @@ -574,9 +577,9 @@ describe('UserGroup Effects', () => { }); it('should return CreateUserGroupOrderApprovalPermissionFail action if user not assigned', () => { - userGroupConnector.assignMember = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.assignMember = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.AssignMember({ userId, userGroupId, @@ -623,9 +626,9 @@ describe('UserGroup Effects', () => { }); it('should return DeleteUserGroupMemberSuccessFail action if users not unassigned', () => { - userGroupConnector.unassignMember = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.unassignMember = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.UnassignMember({ userId, userGroupId, @@ -670,9 +673,9 @@ describe('UserGroup Effects', () => { }); it('should return DeleteUserGroupMemberSuccessFail action if users not unassigned', () => { - userGroupConnector.unassignAllMembers = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + userGroupConnector.unassignAllMembers = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new UserGroupActions.UnassignAllMembers({ userId, userGroupId, diff --git a/feature-libs/organization/administration/core/store/selectors/b2b-user.selector.spec.ts b/feature-libs/organization/administration/core/store/selectors/b2b-user.selector.spec.ts index d64a2276330..af1c5631225 100644 --- a/feature-libs/organization/administration/core/store/selectors/b2b-user.selector.spec.ts +++ b/feature-libs/organization/administration/core/store/selectors/b2b-user.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { B2BUser, StateUtils } from '@spartacus/core'; @@ -51,7 +52,7 @@ describe('B2BUser Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getB2BUserManagementState ', () => { diff --git a/feature-libs/organization/administration/core/store/selectors/budget.selector.spec.ts b/feature-libs/organization/administration/core/store/selectors/budget.selector.spec.ts index b70acd5ceb7..d0569b71526 100644 --- a/feature-libs/organization/administration/core/store/selectors/budget.selector.spec.ts +++ b/feature-libs/organization/administration/core/store/selectors/budget.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { StateUtils } from '@spartacus/core'; @@ -51,7 +52,7 @@ describe('Budget Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getBudgetManagementState ', () => { diff --git a/feature-libs/organization/administration/core/store/selectors/cost-center.selector.spec.ts b/feature-libs/organization/administration/core/store/selectors/cost-center.selector.spec.ts index 4f9e5d8319a..18d56a11547 100644 --- a/feature-libs/organization/administration/core/store/selectors/cost-center.selector.spec.ts +++ b/feature-libs/organization/administration/core/store/selectors/cost-center.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { CostCenter, StateUtils } from '@spartacus/core'; @@ -50,7 +51,7 @@ describe('CostCenter Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getCostCenterManagementState ', () => { diff --git a/feature-libs/organization/administration/core/store/selectors/org-unit.selector.spec.ts b/feature-libs/organization/administration/core/store/selectors/org-unit.selector.spec.ts index ced51ee8273..058c3a78292 100644 --- a/feature-libs/organization/administration/core/store/selectors/org-unit.selector.spec.ts +++ b/feature-libs/organization/administration/core/store/selectors/org-unit.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { Address, B2BUnit, ListModel, StateUtils } from '@spartacus/core'; @@ -61,7 +62,7 @@ describe('OrgUnit Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getOrgUnitsState ', () => { diff --git a/feature-libs/organization/administration/core/store/selectors/permission.selector.spec.ts b/feature-libs/organization/administration/core/store/selectors/permission.selector.spec.ts index f94399a068f..a706b1072b9 100644 --- a/feature-libs/organization/administration/core/store/selectors/permission.selector.spec.ts +++ b/feature-libs/organization/administration/core/store/selectors/permission.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { StateUtils, OrderApprovalPermissionType } from '@spartacus/core'; @@ -63,7 +64,7 @@ describe('Permission Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getPermissionManagementState ', () => { diff --git a/feature-libs/organization/administration/core/store/selectors/user-group.selector.spec.ts b/feature-libs/organization/administration/core/store/selectors/user-group.selector.spec.ts index 22e8dd7102e..d0a599d40b2 100644 --- a/feature-libs/organization/administration/core/store/selectors/user-group.selector.spec.ts +++ b/feature-libs/organization/administration/core/store/selectors/user-group.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { StateUtils } from '@spartacus/core'; @@ -51,7 +52,7 @@ describe('UserGroup Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getUserGroupManagementState ', () => { diff --git a/feature-libs/organization/administration/occ/adapters/occ-b2b-users.adapter.spec.ts b/feature-libs/organization/administration/occ/adapters/occ-b2b-users.adapter.spec.ts index 29defc9da38..d6a7c223fa5 100644 --- a/feature-libs/organization/administration/occ/adapters/occ-b2b-users.adapter.spec.ts +++ b/feature-libs/organization/administration/occ/adapters/occ-b2b-users.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -23,8 +24,6 @@ import { withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; - const userId = 'userId'; const orgCustomerId = 'orgCustomerId'; const orgCustomer: B2BUser = { @@ -38,7 +37,7 @@ const userGroupId = 'userGroupId'; const params: SearchConfig = { sort: 'code' }; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { userId } }) => url === 'b2bUser' ? `${url}/${userId}` : url @@ -69,8 +68,8 @@ describe('OccB2BUserAdapter', () => { httpMock = TestBed.inject( HttpTestingController as Type ); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'convert'); }); afterEach(() => { diff --git a/feature-libs/organization/administration/occ/adapters/occ-budget.adapter.spec.ts b/feature-libs/organization/administration/occ/adapters/occ-budget.adapter.spec.ts index ec919e9acc6..d3b8c9c1143 100644 --- a/feature-libs/organization/administration/occ/adapters/occ-budget.adapter.spec.ts +++ b/feature-libs/organization/administration/occ/adapters/occ-budget.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -14,8 +15,6 @@ import { withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; - const budgetCode = 'testCode'; const userId = 'userId'; const budget = { @@ -25,7 +24,7 @@ const budget = { }; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { budgetCode } }) => url === 'budget' ? url + budgetCode : url @@ -52,7 +51,7 @@ describe('OccBudgetAdapter', () => { converterService = TestBed.inject(ConverterService); service = TestBed.inject(OccBudgetAdapter); httpMock = TestBed.inject(HttpTestingController); - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); }); afterEach(() => { diff --git a/feature-libs/organization/administration/occ/adapters/occ-cost-center.adapter.spec.ts b/feature-libs/organization/administration/occ/adapters/occ-cost-center.adapter.spec.ts index 1a7a06bdb3c..55238bfa175 100644 --- a/feature-libs/organization/administration/occ/adapters/occ-cost-center.adapter.spec.ts +++ b/feature-libs/organization/administration/occ/adapters/occ-cost-center.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -16,8 +17,6 @@ import { withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; - const costCenterCode = 'testCode'; const budgetCode = 'budgetCode'; const userId = 'userId'; @@ -30,7 +29,7 @@ const budget = { code: budgetCode, }; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { costCenterCode } }) => url === 'costCenter' ? url + costCenterCode : url @@ -57,7 +56,7 @@ describe('OccCostCenterAdapter', () => { converterService = TestBed.inject(ConverterService); service = TestBed.inject(OccCostCenterAdapter); httpMock = TestBed.inject(HttpTestingController); - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); }); afterEach(() => { diff --git a/feature-libs/organization/administration/occ/adapters/occ-org-unit.adapter.spec.ts b/feature-libs/organization/administration/occ/adapters/occ-org-unit.adapter.spec.ts index f59f3fbd85e..bfe369486e3 100644 --- a/feature-libs/organization/administration/occ/adapters/occ-org-unit.adapter.spec.ts +++ b/feature-libs/organization/administration/occ/adapters/occ-org-unit.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -25,7 +26,6 @@ import { provideHttpClient, withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; const orgUnitId = 'testId'; const userId = 'userId'; @@ -40,7 +40,7 @@ const address: Address = { id: 'testAddressId' }; const addressId: string = address.id; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { orgUnitId } }) => url === 'orgUnit' ? url + orgUnitId : url @@ -67,8 +67,8 @@ describe('OccOrgUnitAdapter', () => { converterService = TestBed.inject(ConverterService); service = TestBed.inject(OccOrgUnitAdapter); httpMock = TestBed.inject(HttpTestingController); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'convert'); }); afterEach(() => { diff --git a/feature-libs/organization/administration/occ/adapters/occ-permission.adapter.spec.ts b/feature-libs/organization/administration/occ/adapters/occ-permission.adapter.spec.ts index 25ad6b5309f..3c99f910a77 100644 --- a/feature-libs/organization/administration/occ/adapters/occ-permission.adapter.spec.ts +++ b/feature-libs/organization/administration/occ/adapters/occ-permission.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -15,8 +16,6 @@ import { withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; - const orderApprovalPermissionCode = 'testCode'; const userId = 'userId'; const permission = { @@ -25,7 +24,7 @@ const permission = { }; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { orderApprovalPermissionCode } }) => url === 'permission' ? url + orderApprovalPermissionCode : url @@ -52,8 +51,8 @@ describe('OccPermissionAdapter', () => { converterService = TestBed.inject(ConverterService); service = TestBed.inject(OccPermissionAdapter); httpMock = TestBed.inject(HttpTestingController); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'convert'); }); afterEach(() => { diff --git a/feature-libs/organization/administration/occ/adapters/occ-user-group.adapter.spec.ts b/feature-libs/organization/administration/occ/adapters/occ-user-group.adapter.spec.ts index 7d01393126b..9e839b54910 100644 --- a/feature-libs/organization/administration/occ/adapters/occ-user-group.adapter.spec.ts +++ b/feature-libs/organization/administration/occ/adapters/occ-user-group.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -18,8 +19,6 @@ import { withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; - const userGroupId = 'testUid'; const permissionUid = 'permissionUid'; const memberUid = 'memberUid'; @@ -36,7 +35,7 @@ const member = { }; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { userGroupId } }) => url === 'userGroup' ? url + userGroupId : url @@ -67,8 +66,8 @@ describe('OccUserGroupAdapter', () => { httpMock = TestBed.inject( HttpTestingController as Type ); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'convert'); }); afterEach(() => { diff --git a/feature-libs/organization/karma.conf.js b/feature-libs/organization/karma.conf.js deleted file mode 100644 index e8d6a265d33..00000000000 --- a/feature-libs/organization/karma.conf.js +++ /dev/null @@ -1,52 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-organization.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/organization'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 85, - lines: 85, - branches: 70, - functions: 80, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/organization/order-approval/components/details/order-approval-detail-form/order-approval-detail-form.component.spec.ts b/feature-libs/organization/order-approval/components/details/order-approval-detail-form/order-approval-detail-form.component.spec.ts index c6c7135e21e..a9be0eb4528 100644 --- a/feature-libs/organization/order-approval/components/details/order-approval-detail-form/order-approval-detail-form.component.spec.ts +++ b/feature-libs/organization/order-approval/components/details/order-approval-detail-form/order-approval-detail-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, DebugElement, @@ -175,16 +176,16 @@ describe('OrderApprovalDetailFormComponent', () => { it('should have comment as optional for approval.', () => { displayDecisionForm(APPROVE); - expect(component.approvalForm.valid).toBeTrue(); + expect(component.approvalForm.valid).toBe(true); }); it('should have comment as optional for rejection.', () => { displayDecisionForm(REJECT); - expect(component.approvalForm.valid).toBeFalse(); + expect(component.approvalForm.valid).toBe(false); }); it('should not submit rejection without comment.', () => { - spyOn(orderApprovalService, 'makeDecision').and.stub(); + vi.spyOn(orderApprovalService, 'makeDecision').mockImplementation(() => {}); displayDecisionForm(REJECT); clickButton('orderApprovalDetails.form.submit_' + REJECT); expect(orderApprovalService.makeDecision).not.toHaveBeenCalled(); @@ -235,7 +236,7 @@ describe('OrderApprovalDetailFormComponent', () => { } function submitDecisionForm(decision: OrderApprovalDecisionValue) { - spyOn(orderApprovalService, 'makeDecision').and.stub(); + vi.spyOn(orderApprovalService, 'makeDecision').mockImplementation(() => {}); const testComment = 'Decision comment ' + decision; component.approvalForm.controls.comment.setValue(testComment); clickButton('orderApprovalDetails.form.submit_' + decision); diff --git a/feature-libs/organization/order-approval/components/details/order-detail-permission-results/order-detail-permission-results.component.spec.ts b/feature-libs/organization/order-approval/components/details/order-detail-permission-results/order-detail-permission-results.component.spec.ts index a647ef2c3c3..5beb8967b95 100644 --- a/feature-libs/organization/order-approval/components/details/order-detail-permission-results/order-detail-permission-results.component.spec.ts +++ b/feature-libs/organization/order-approval/components/details/order-detail-permission-results/order-detail-permission-results.component.spec.ts @@ -86,20 +86,24 @@ describe('OrderDetailPermissionResultsComponent', () => { for (let i = 0; i < mockOrder.permissionResults.length; i++) { expect( - element.query( - By.css(`tr:nth-of-type(${i + 1}) td.cx-approval-approverName`) - ).nativeElement.innerText + element + .query(By.css(`tr:nth-of-type(${i + 1}) td.cx-approval-approverName`)) + .nativeElement.textContent?.trim() ).toContain(mockOrder.permissionResults[i].approverName); expect( - element.query( - By.css(`tr:nth-of-type(${i + 1}) td.cx-approval-statusDisplay`) - ).nativeElement.innerText + element + .query( + By.css(`tr:nth-of-type(${i + 1}) td.cx-approval-statusDisplay`) + ) + .nativeElement.textContent?.trim() ).toContain(mockOrder.permissionResults[i].statusDisplay); expect( - element.query( - By.css(`tr:nth-of-type(${i + 1}) td.cx-approval-approvalNotes`) - ).nativeElement.innerText + element + .query( + By.css(`tr:nth-of-type(${i + 1}) td.cx-approval-approvalNotes`) + ) + .nativeElement.textContent?.trim() ).toContain( mockOrder.permissionResults[i].approverNotes || 'orderApprovalDetails.permissionResults.noApprovalComments' diff --git a/feature-libs/organization/order-approval/components/list/order-approval-list.component.spec.ts b/feature-libs/organization/order-approval/components/list/order-approval-list.component.spec.ts index 6018bc987d0..9aecd7c5672 100644 --- a/feature-libs/organization/order-approval/components/list/order-approval-list.component.spec.ts +++ b/feature-libs/organization/order-approval/components/list/order-approval-list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, DebugElement, @@ -27,7 +28,6 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { OrderApproval } from '../../core/model/order-approval.model'; import { OrderApprovalService } from '../../core/services/order-approval.service'; import { OrderApprovalListComponent } from './order-approval-list.component'; -import createSpy = jasmine.createSpy; const mockOrderApprovals: EntitiesModel = { pagination: { @@ -106,7 +106,7 @@ class MockOrderApprovalService { } class MockRoutingService { - go = createSpy('go').and.stub(); + go = vi.fn().mockImplementation(() => {}); } describe('OrderApprovalListComponent?', () => { @@ -182,7 +182,7 @@ describe('OrderApprovalListComponent?', () => { }); it('should set correctly sort code', () => { - spyOn(orderApprovalService, 'getList').and.stub(); + vi.spyOn(orderApprovalService, 'getList').mockImplementation(() => {}); component.changeSortCode('byOrderNumber'); @@ -200,7 +200,9 @@ describe('OrderApprovalListComponent?', () => { }); it('should set correctly page', () => { - spyOn(orderApprovalService, 'loadOrderApprovals').and.stub(); + vi.spyOn(orderApprovalService, 'loadOrderApprovals').mockImplementation( + () => {} + ); component.sortType = 'byDate'; component.pageChange(1); diff --git a/feature-libs/organization/order-approval/core/connectors/order-approval.connector.spec.ts b/feature-libs/organization/order-approval/core/connectors/order-approval.connector.spec.ts index ba3fb3df5a0..4ec2fc65d9e 100644 --- a/feature-libs/organization/order-approval/core/connectors/order-approval.connector.spec.ts +++ b/feature-libs/organization/order-approval/core/connectors/order-approval.connector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { SearchConfig } from '@spartacus/core'; import { of } from 'rxjs'; @@ -7,7 +8,6 @@ import { } from '../../core/model/order-approval.model'; import { OrderApprovalAdapter } from './order-approval.adapter'; import { OrderApprovalConnector } from './order-approval.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId'; const orderApprovalCode = 'orderApprovalCode'; @@ -22,15 +22,9 @@ const orderApprvalDecision: OrderApprovalDecision = { }; class MockOrderApprovalAdapter implements OrderApprovalAdapter { - load = createSpy('OrderApprovalAdapter.load').and.returnValue( - of(orderApproval) - ); - loadList = createSpy('OrderApprovalAdapter.loadList').and.returnValue( - of([orderApproval]) - ); - makeDecision = createSpy('OrderApprovalAdapter.makeDecision').and.returnValue( - of(orderApprvalDecision) - ); + load = vi.fn().mockReturnValue(of(orderApproval)); + loadList = vi.fn().mockReturnValue(of([orderApproval])); + makeDecision = vi.fn().mockReturnValue(of(orderApprvalDecision)); } describe('OrderApprovalConnector', () => { diff --git a/feature-libs/organization/order-approval/core/guards/approver.guard.spec.ts b/feature-libs/organization/order-approval/core/guards/approver.guard.spec.ts index b8fa2d37325..af7c0682fe5 100644 --- a/feature-libs/organization/order-approval/core/guards/approver.guard.spec.ts +++ b/feature-libs/organization/order-approval/core/guards/approver.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { B2BUserRole, @@ -9,22 +10,21 @@ import { import { UserAccountFacade } from '@spartacus/user/account/root'; import { of } from 'rxjs'; import { ApproverGuard } from './approver.guard'; -import createSpy = jasmine.createSpy; const mockUserDetails: User = { roles: [], }; class MockUserAccountFacade implements Partial { - get = createSpy('get').and.returnValue(of(mockUserDetails)); + get = vi.fn().mockReturnValue(of(mockUserDetails)); } class MockRoutingService implements Partial { - go = createSpy('go'); + go = vi.fn(); } class MockGlobalMessageService implements Partial { - add = createSpy('add'); + add = vi.fn(); } describe('ApproverGuard', () => { diff --git a/feature-libs/organization/order-approval/core/services/order-approval.service.spec.ts b/feature-libs/organization/order-approval/core/services/order-approval.service.spec.ts index 25cf8e5d769..04781ae5ba9 100644 --- a/feature-libs/organization/order-approval/core/services/order-approval.service.spec.ts +++ b/feature-libs/organization/order-approval/core/services/order-approval.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { @@ -19,7 +20,6 @@ import { import * as fromReducers from '../store/reducers/index'; import { OrderApprovalService } from './order-approval.service'; -import createSpy = jasmine.createSpy; import { of } from 'rxjs'; const userId = 'current'; @@ -39,7 +39,7 @@ const orderApprovalDecision: OrderApprovalDecision = { }; class MockUserIdService implements Partial { - takeUserId = createSpy().and.callFake(() => { + takeUserId = vi.fn().mockImplementation(() => { return of(userId); }); } @@ -68,7 +68,7 @@ describe('OrderApprovalService', () => { store = TestBed.inject(Store); service = TestBed.inject(OrderApprovalService); userIdService = TestBed.inject(UserIdService); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); it('should OrderApprovalService is injected', inject( diff --git a/feature-libs/organization/order-approval/core/store/effects/order-approval.effect.spec.ts b/feature-libs/organization/order-approval/core/store/effects/order-approval.effect.spec.ts index 67cd7f036e1..78f1a0a3d91 100644 --- a/feature-libs/organization/order-approval/core/store/effects/order-approval.effect.spec.ts +++ b/feature-libs/organization/order-approval/core/store/effects/order-approval.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpHeaders, @@ -26,8 +27,6 @@ import { import { OrderApprovalActions } from '../actions/index'; import * as fromEffects from './order-approval.effect'; -import createSpy = jasmine.createSpy; - const httpErrorResponse = new HttpErrorResponse({ error: 'error', headers: new HttpHeaders().set('xxx', 'xxx'), @@ -51,11 +50,11 @@ const pagination = { currentPage: 1 }; const sorts = [{ selected: true, name: 'code' }]; class MockOrderApprovalConnector { - get = createSpy().and.returnValue(of(orderApproval)); - getList = createSpy().and.returnValue( - of({ values: [orderApproval], pagination, sorts }) - ); - makeDecision = createSpy().and.returnValue(of(orderApprovalDecision)); + get = vi.fn().mockReturnValue(of(orderApproval)); + getList = vi + .fn() + .mockReturnValue(of({ values: [orderApproval], pagination, sorts })); + makeDecision = vi.fn().mockReturnValue(of(orderApprovalDecision)); } class MockLoggerService { @@ -136,9 +135,9 @@ describe('OrderApproval Effects', () => { }); it('should return LoadOrderApprovalFail action if orderApproval not updated', () => { - orderApprovalConnector.get = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orderApprovalConnector.get = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrderApprovalActions.LoadOrderApproval({ userId, orderApprovalCode, @@ -184,9 +183,9 @@ describe('OrderApproval Effects', () => { }); it('should return LoadOrderApprovalsFail action if orderApprovals not loaded', () => { - orderApprovalConnector.getList = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + orderApprovalConnector.getList = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrderApprovalActions.LoadOrderApprovals({ userId, params, @@ -233,9 +232,9 @@ describe('OrderApproval Effects', () => { }); it('should return MakeDecisionFail action if decision not created', () => { - orderApprovalConnector.makeDecision = createSpy( - 'makeDecision' - ).and.returnValue(throwError(() => httpErrorResponse)); + orderApprovalConnector.makeDecision = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); const action = new OrderApprovalActions.MakeDecision({ userId, orderApprovalCode, diff --git a/feature-libs/organization/order-approval/core/store/selectors/order-approval.selector.spec.ts b/feature-libs/organization/order-approval/core/store/selectors/order-approval.selector.spec.ts index 1f0c5bea7f5..f63b7146ebb 100644 --- a/feature-libs/organization/order-approval/core/store/selectors/order-approval.selector.spec.ts +++ b/feature-libs/organization/order-approval/core/store/selectors/order-approval.selector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { StateUtils } from '@spartacus/core'; @@ -49,7 +50,7 @@ describe('OrderApproval Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getOrderApprovalManagementState ', () => { diff --git a/feature-libs/organization/order-approval/occ/adapters/occ-order-approval.adapter.spec.ts b/feature-libs/organization/order-approval/occ/adapters/occ-order-approval.adapter.spec.ts index 41cff6add24..2b571c895f9 100644 --- a/feature-libs/organization/order-approval/occ/adapters/occ-order-approval.adapter.spec.ts +++ b/feature-libs/organization/order-approval/occ/adapters/occ-order-approval.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -20,8 +21,6 @@ import { withInterceptorsFromDi, } from '@angular/common/http'; -import createSpy = jasmine.createSpy; - const orderApprovalCode = 'testCode'; const userId = 'userId'; const orderApproval: OrderApproval = { @@ -34,7 +33,7 @@ const orderApprovalDecision: OrderApprovalDecision = { }; class MockOccEndpointsService { - buildUrl = createSpy('MockOccEndpointsService.buildUrl').and.callFake( + buildUrl = vi.fn().mockImplementation( // eslint-disable-next-line @typescript-eslint/no-shadow (url, { urlParams: { orderApprovalCode } }) => url === 'orderApproval' || url === 'orderApprovalDecision' @@ -63,7 +62,7 @@ describe('OccOrderApprovalAdapter', () => { converterService = TestBed.inject(ConverterService); service = TestBed.inject(OccOrderApprovalAdapter); httpMock = TestBed.inject(HttpTestingController); - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); }); afterEach(() => { diff --git a/feature-libs/organization/order-approval/occ/converters/occ-order-approval-normalizer.spec.ts b/feature-libs/organization/order-approval/occ/converters/occ-order-approval-normalizer.spec.ts index 662e7946588..065b79a70aa 100644 --- a/feature-libs/organization/order-approval/occ/converters/occ-order-approval-normalizer.spec.ts +++ b/feature-libs/organization/order-approval/occ/converters/occ-order-approval-normalizer.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Type } from '@angular/core'; import { inject, TestBed } from '@angular/core/testing'; import { ConverterService, Occ, OccConfig } from '@spartacus/core'; @@ -52,7 +53,7 @@ describe('OrderApprovalNormalizer', () => { OccOrderApprovalNormalizer as Type ); converter = TestBed.inject(ConverterService); - spyOn(converter, 'convert').and.callFake( + vi.spyOn(converter, 'convert').mockImplementation( (order) => ({ ...order, diff --git a/feature-libs/organization/project.json b/feature-libs/organization/project.json index 7cecd7bf7c4..86dd62aba36 100644 --- a/feature-libs/organization/project.json +++ b/feature-libs/organization/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/organization/test.ts", - "tsConfig": "feature-libs/organization/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/organization/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/organization/test.ts b/feature-libs/organization/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/organization/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/organization/tsconfig.spec.json b/feature-libs/organization/tsconfig.spec.json index 3c36fd6d4e0..d52c68cbde6 100644 --- a/feature-libs/organization/tsconfig.spec.json +++ b/feature-libs/organization/tsconfig.spec.json @@ -2,11 +2,18 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "strict": false, "module": "preserve", - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "strict": false, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-detail.service.spec.ts b/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-detail.service.spec.ts index 960eefda1b9..18b08b84003 100644 --- a/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-detail.service.spec.ts +++ b/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-detail.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { RoutingService } from '@spartacus/core'; import { Order } from '@spartacus/order/root'; @@ -69,10 +70,10 @@ describe('UnitLevelOrderDetailService', () => { unitOrderFacade = TestBed.inject(UnitOrderFacade); routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'getRouterState'); - spyOn(unitOrderFacade, 'loadOrderDetails'); - spyOn(unitOrderFacade, 'clearOrderDetails'); - spyOn(unitOrderFacade, 'getOrderDetails').and.returnValue(of(mockOrder)); + vi.spyOn(routingService, 'getRouterState'); + vi.spyOn(unitOrderFacade, 'loadOrderDetails'); + vi.spyOn(unitOrderFacade, 'clearOrderDetails'); + vi.spyOn(unitOrderFacade, 'getOrderDetails').mockReturnValue(of(mockOrder)); }); it('should be created', () => { diff --git a/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-overview/unit-level-order-overview.component.spec.ts b/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-overview/unit-level-order-overview.component.spec.ts index 448f8f35c01..85aaa66762d 100644 --- a/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-overview/unit-level-order-overview.component.spec.ts +++ b/feature-libs/organization/unit-order/components/unit-level-order-detail/unit-level-order-overview/unit-level-order-overview.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeliveryMode } from '@spartacus/cart/base/root'; @@ -161,18 +162,18 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getOrderDetails', () => { - spyOn(orderDetailService, 'getOrderDetails').and.callThrough(); + vi.spyOn(orderDetailService, 'getOrderDetails'); component.ngOnInit(); expect(orderDetailService.getOrderDetails).toHaveBeenCalled(); }); describe('when replenishment is NOT defined', () => { beforeEach(() => { - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); }); it('should call getOrderCodeCardContent(orderCode: string)', () => { - spyOn(component, 'getOrderCodeCardContent').and.callThrough(); + vi.spyOn(component, 'getOrderCodeCardContent'); component .getOrderCodeCardContent(mockOrder.code) @@ -189,7 +190,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getOrderCurrentDateCardContent(isoDate: string)', () => { - spyOn(component, 'getOrderCurrentDateCardContent').and.callThrough(); + vi.spyOn(component, 'getOrderCurrentDateCardContent'); const date = mockOrder.created.toDateString(); @@ -206,7 +207,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getOrderStatusCardContent(status: string)', () => { - spyOn(component, 'getOrderStatusCardContent').and.callThrough(); + vi.spyOn(component, 'getOrderStatusCardContent'); component .getOrderStatusCardContent(mockOrder.statusDisplay) @@ -225,11 +226,11 @@ describe('UnitLevelOrderOverviewComponent', () => { describe('when purchase order number is defined', () => { beforeEach(() => { - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); }); it('should call getPurchaseOrderNumber(poNumber: string)', () => { - spyOn(component, 'getPurchaseOrderNumber').and.callThrough(); + vi.spyOn(component, 'getPurchaseOrderNumber'); component .getPurchaseOrderNumber(mockOrder.purchaseOrderNumber) @@ -246,7 +247,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getMethodOfPaymentCardContent(hasPaymentInfo: PaymentDetails)', () => { - spyOn(component, 'getMethodOfPaymentCardContent').and.callThrough(); + vi.spyOn(component, 'getMethodOfPaymentCardContent'); component .getMethodOfPaymentCardContent(mockOrder.paymentInfo) @@ -263,7 +264,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getCostCenterCardContent(costCenter: CostCenter)', () => { - spyOn(component, 'getCostCenterCardContent').and.callThrough(); + vi.spyOn(component, 'getCostCenterCardContent'); component .getCostCenterCardContent(mockOrder.costCenter) @@ -283,11 +284,11 @@ describe('UnitLevelOrderOverviewComponent', () => { describe('when paymentInfo is defined', () => { beforeEach(() => { - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); }); it('should call getPaymentInfoCardContent(payment: PaymentDetails)', () => { - spyOn(component, 'getPaymentInfoCardContent').and.callThrough(); + vi.spyOn(component, 'getPaymentInfoCardContent'); component .getPaymentInfoCardContent(mockOrder.paymentInfo) @@ -307,7 +308,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getBillingAddressCardContent(billingAddress: Address)', () => { - spyOn(component, 'getBillingAddressCardContent').and.callThrough(); + vi.spyOn(component, 'getBillingAddressCardContent'); const billingAddress = mockOrder.paymentInfo.billingAddress as Address; @@ -334,11 +335,11 @@ describe('UnitLevelOrderOverviewComponent', () => { describe('common column in all types of order', () => { beforeEach(() => { - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); }); it('should call getAddressCardContent(deliveryAddress: Address)', () => { - spyOn(component, 'getAddressCardContent').and.callThrough(); + vi.spyOn(component, 'getAddressCardContent'); const deliveryAddress = mockOrder.deliveryAddress; @@ -363,7 +364,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getDeliveryModeCardContent(deliveryMode: DeliveryMode)', () => { - spyOn(component, 'getDeliveryModeCardContent').and.callThrough(); + vi.spyOn(component, 'getDeliveryModeCardContent'); component .getDeliveryModeCardContent(mockOrder.deliveryMode) @@ -403,11 +404,11 @@ describe('UnitLevelOrderOverviewComponent', () => { describe('when unit order is defined', () => { beforeEach(() => { - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); }); it('should call getBuyerNameCardContent(customer: B2BUser)', () => { - spyOn(component, 'getBuyerNameCardContent').and.callThrough(); + vi.spyOn(component, 'getBuyerNameCardContent'); component .getBuyerNameCardContent(mockOrder.orgCustomer) @@ -427,7 +428,7 @@ describe('UnitLevelOrderOverviewComponent', () => { }); it('should call getUnitNameCardContent(orgUnit: string)', () => { - spyOn(component, 'getUnitNameCardContent').and.callThrough(); + vi.spyOn(component, 'getUnitNameCardContent'); component .getUnitNameCardContent(mockOrder.orgUnit.name as string) diff --git a/feature-libs/organization/unit-order/components/unit-level-order-history/filter/unit-level-order-history-filter.component.spec.ts b/feature-libs/organization/unit-order/components/unit-level-order-history/filter/unit-level-order-history-filter.component.spec.ts index 4d2bf7a3b14..0c10314c583 100644 --- a/feature-libs/organization/unit-order/components/unit-level-order-history/filter/unit-level-order-history-filter.component.spec.ts +++ b/feature-libs/organization/unit-order/components/unit-level-order-history/filter/unit-level-order-history-filter.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, EventEmitter, @@ -6,12 +7,7 @@ import { Pipe, PipeTransform, } from '@angular/core'; -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { PaginationModel, TranslatePipe } from '@spartacus/core'; @@ -65,7 +61,7 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { filters: '', }; - beforeEach(fakeAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, UnitLevelOrderHistoryFilterComponent], }) @@ -86,7 +82,7 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { fixture = TestBed.createComponent(UnitLevelOrderHistoryFilterComponent); component = fixture.componentInstance; fixture.detectChanges(); - })); + }); it('should create', () => { expect(component).toBeTruthy(); @@ -94,8 +90,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { describe('desktop view', () => { it('should emit buyer when filtered by buyer', () => { - const spy = spyOn(component, 'searchUnitLevelOrders').and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'searchUnitLevelOrders'); + vi.spyOn(component.filterListEvent, 'emit'); const searchBtn = fixture.debugElement.query( By.css('#searchUnitLevelOrdersBtn') @@ -125,8 +121,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should emit unit when filtered by unit', () => { - const spy = spyOn(component, 'searchUnitLevelOrders').and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'searchUnitLevelOrders'); + vi.spyOn(component.filterListEvent, 'emit'); const searchBtn = fixture.debugElement.query( By.css('#searchUnitLevelOrdersBtn') @@ -156,8 +152,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should emit a buyer and a unit when filtered by buyer and unit', () => { - const spy = spyOn(component, 'searchUnitLevelOrders').and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'searchUnitLevelOrders'); + vi.spyOn(component.filterListEvent, 'emit'); const searchBtn = fixture.debugElement.query( By.css('#searchUnitLevelOrdersBtn') @@ -187,7 +183,7 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should clear all of the filtered values when clearAll button is clicked', () => { - const spy = spyOn(component, 'clearAll').and.callThrough(); + const spy = vi.spyOn(component, 'clearAll'); const clearbtn = fixture.debugElement.query(By.css('#clearAllBtn')); const form = component.filterForm; form.patchValue({ @@ -202,8 +198,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should clear the unit value when x button in the unit-input field is clicked', () => { - const spy = spyOn(component, 'clearUnit').and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'clearUnit'); + vi.spyOn(component.filterListEvent, 'emit'); const form = component.filterForm; form.patchValue({ @@ -231,8 +227,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should clear the buyer value when x button in the buyer-input field is clicked', () => { - const spy = spyOn(component, 'clearBuyer').and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'clearBuyer'); + vi.spyOn(component.filterListEvent, 'emit'); const form = component.filterForm; form.patchValue({ @@ -260,7 +256,7 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should emit the filter event', () => { - spyOn(component.filterListEvent, 'emit'); + vi.spyOn(component.filterListEvent, 'emit'); component.emitFilterEvent(GI, SERVICES); fixture.detectChanges(); @@ -273,11 +269,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { describe('mobile view', () => { it('should emit a buyer value when filtered by a buyer', () => { - const spy = spyOn( - component, - 'searchUnitLevelOrdersForMobile' - ).and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'searchUnitLevelOrdersForMobile'); + vi.spyOn(component.filterListEvent, 'emit'); const form = component.filterFormMobile; form.patchValue({ @@ -304,11 +297,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should emit a unit value when filtered by a unit', () => { - const spy = spyOn( - component, - 'searchUnitLevelOrdersForMobile' - ).and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'searchUnitLevelOrdersForMobile'); + vi.spyOn(component.filterListEvent, 'emit'); const form = component.filterFormMobile; form.patchValue({ @@ -334,11 +324,8 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should emit a buyer and a unit value when filtered by buyer and unit', () => { - const spy = spyOn( - component, - 'searchUnitLevelOrdersForMobile' - ).and.callThrough(); - spyOn(component.filterListEvent, 'emit'); + const spy = vi.spyOn(component, 'searchUnitLevelOrdersForMobile'); + vi.spyOn(component.filterListEvent, 'emit'); const form = component.filterFormMobile; form.patchValue({ @@ -364,32 +351,34 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should remove all of the filtered values when clicked on Remove Applied Filter button', () => { - const form = component.filterFormMobile; - form.patchValue({ - buyerFilterMobile: GI, - unitFilterMobile: SERVICES, - }); - spyOn(component, 'searchUnitLevelOrdersForMobile').and.callThrough(); - component.searchUnitLevelOrdersForMobile(); - fixture.detectChanges(); + // Create a fresh fixture so buyerFilterMobileValue starts as 'gi' before + // the first detectChanges, avoiding NG0100 (value changed between cycles) + const freshFixture = TestBed.createComponent( + UnitLevelOrderHistoryFilterComponent + ); + const freshComponent = freshFixture.componentInstance; + freshComponent['buyerFilterMobileValue'] = GI; + freshComponent['unitFilterMobileValue'] = SERVICES; + freshFixture.detectChanges(); - const spy = spyOn(component, 'clearAll').and.callThrough(); - fixture.debugElement + const spy = vi.spyOn(freshComponent, 'clearAll'); + freshFixture.debugElement .query(By.css('#removeAppliedFiltersBtn')) .nativeElement.click(); - fixture.detectChanges(); + freshFixture.detectChanges(); expect(spy).toHaveBeenCalledTimes(1); - expect(component.buyerFilterMobileId.nativeElement.value).toBe( + expect(freshComponent.buyerFilterMobileId.nativeElement.value).toBe( EMPTY_STRING ); - expect(component.unitFilterMobileId.nativeElement.value).toBe( + expect(freshComponent.unitFilterMobileId.nativeElement.value).toBe( EMPTY_STRING ); }); - it('should clear unit value when clicked on x button in the searchByUnit field', fakeAsync(() => { - const spy = spyOn(component, 'clearUnitMobile').and.callThrough(); + it('should clear unit value when clicked on x button in the searchByUnit field', async () => { + vi.useFakeTimers(); + const spy = vi.spyOn(component, 'clearUnitMobile'); const form = component.filterFormMobile; form.patchValue({ buyerFilterMobile: GI, @@ -404,14 +393,16 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { ); fixture.detectChanges(); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(spy).toHaveBeenCalled(); expect(form.get(BUYER_FILTER_MOBILE)?.value).toBe(GI); expect(form.get(UNIT_FILTER_MOBILE)?.value).toBeNull(); - })); + }); - it('should clear buyer value when clicked on x button in the searchByBuyer field', fakeAsync(() => { - const spy = spyOn(component, 'clearBuyerMobile').and.callThrough(); + it('should clear buyer value when clicked on x button in the searchByBuyer field', async () => { + vi.useFakeTimers(); + const spy = vi.spyOn(component, 'clearBuyerMobile'); const form = component.filterFormMobile; form.patchValue({ buyerFilterMobile: GI, @@ -426,14 +417,15 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { ); fixture.detectChanges(); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(spy).toHaveBeenCalled(); expect(form.get(BUYER_FILTER_MOBILE)?.value).toBeNull(); expect(form.get(UNIT_FILTER_MOBILE)?.value).toBe(SERVICES); - })); + }); it('should call launchMobileFilters when filterBy button is clicked', () => { - const spy = spyOn(component, 'launchMobileFilters').and.callThrough(); + const spy = vi.spyOn(component, 'launchMobileFilters'); fixture.detectChanges(); const filterByBtn = fixture.debugElement.query(By.css('#filterByBtn')); filterByBtn.nativeElement.click(); @@ -441,7 +433,7 @@ describe('UnitLevelOrderHistoryFilterComponent', () => { }); it('should call closeFilterNav when close button is clicked on nav', () => { - const spy = spyOn(component, 'closeFilterNav').and.callThrough(); + const spy = vi.spyOn(component, 'closeFilterNav'); fixture.detectChanges(); const closeFilterNavBtn = fixture.debugElement.query( By.css('#closeFilterNavBtn') diff --git a/feature-libs/organization/unit-order/components/unit-level-order-history/unit-level-order-history.component.spec.ts b/feature-libs/organization/unit-order/components/unit-level-order-history/unit-level-order-history.component.spec.ts index 9661476496d..e77ab83dc88 100644 --- a/feature-libs/organization/unit-order/components/unit-level-order-history/unit-level-order-history.component.spec.ts +++ b/feature-libs/organization/unit-order/components/unit-level-order-history/unit-level-order-history.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, EventEmitter, @@ -236,7 +237,7 @@ describe('UnitLevelOrderHistoryComponent', () => { }); it('should redirect when clicking on order id', () => { - spyOn(routingService, 'go').and.stub(); + vi.spyOn(routingService, 'go').mockImplementation(() => {}); fixture.detectChanges(); const rows = fixture.debugElement.queryAll( @@ -252,7 +253,7 @@ describe('UnitLevelOrderHistoryComponent', () => { }); it('should set correctly sort code', () => { - spyOn(unitOrderFacade, 'loadOrderList').and.stub(); + vi.spyOn(unitOrderFacade, 'loadOrderList').mockImplementation(() => {}); component.changeSortCode('byOrderNumber'); @@ -266,7 +267,7 @@ describe('UnitLevelOrderHistoryComponent', () => { }); it('should set correctly page', () => { - spyOn(unitOrderFacade, 'loadOrderList').and.stub(); + vi.spyOn(unitOrderFacade, 'loadOrderList').mockImplementation(() => {}); component.changeSortCode('byDate'); component.pageChange(1); @@ -332,7 +333,7 @@ describe('UnitLevelOrderHistoryComponent', () => { }); it('should clear order history data when component destroy', () => { - spyOn(unitOrderFacade, 'clearOrderList').and.stub(); + vi.spyOn(unitOrderFacade, 'clearOrderList').mockImplementation(() => {}); component.ngOnDestroy(); expect(unitOrderFacade.clearOrderList).toHaveBeenCalledWith(); @@ -352,7 +353,7 @@ describe('UnitLevelOrderHistoryComponent', () => { }); it('should set correct filters', () => { - spyOn(unitOrderFacade, 'loadOrderList').and.stub(); + vi.spyOn(unitOrderFacade, 'loadOrderList').mockImplementation(() => {}); let orderHistoryQueryParam: OrderHistoryQueryParams = { currentPage: 0, diff --git a/feature-libs/organization/unit-order/core/connectors/unit-order.connector.spec.ts b/feature-libs/organization/unit-order/core/connectors/unit-order.connector.spec.ts index d1557374e1f..972a3f2e8f5 100644 --- a/feature-libs/organization/unit-order/core/connectors/unit-order.connector.spec.ts +++ b/feature-libs/organization/unit-order/core/connectors/unit-order.connector.spec.ts @@ -1,19 +1,19 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { UnitOrderAdapter } from './unit-order.adapter'; import { UnitOrderConnector } from './unit-order.connector'; -import createSpy = jasmine.createSpy; class MockUnitOrderAdapter implements Partial { - loadUnitOrderHistory = createSpy( - 'UnitOrderAdapter.loadUnitOrderHistory' - ).and.callFake((userId: string) => of(`orderHistory-${userId}`)); - - loadUnitOrderDetail = createSpy( - 'UnitOrderAdapter.loadUnitOrderDetail' - ).and.callFake((userId: string, orderCode: string) => - of(`orderDetails-${userId}-${orderCode}`) - ); + loadUnitOrderHistory = vi + .fn() + .mockImplementation((userId: string) => of(`orderHistory-${userId}`)); + + loadUnitOrderDetail = vi + .fn() + .mockImplementation((userId: string, orderCode: string) => + of(`orderDetails-${userId}-${orderCode}`) + ); } describe('OrderHistoryConnector', () => { diff --git a/feature-libs/organization/unit-order/core/guards/unit-level-orders-viewer.guard.spec.ts b/feature-libs/organization/unit-order/core/guards/unit-level-orders-viewer.guard.spec.ts index 773bd60dc73..61843cc83f5 100644 --- a/feature-libs/organization/unit-order/core/guards/unit-level-orders-viewer.guard.spec.ts +++ b/feature-libs/organization/unit-order/core/guards/unit-level-orders-viewer.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { B2BUserRole, @@ -10,7 +11,6 @@ import { import { UserAccountFacade } from '@spartacus/user/account/root'; import { of } from 'rxjs'; import { UnitLevelOrdersViewerGuard } from './unit-level-orders-viewer.guard'; -import createSpy = jasmine.createSpy; const mockUserDetails: User = { firstName: 'test', @@ -19,15 +19,15 @@ const mockUserDetails: User = { }; class MockUserAccountFacade implements Partial { - get = createSpy('get').and.returnValue(of(mockUserDetails)); + get = vi.fn().mockReturnValue(of(mockUserDetails)); } class MockRoutingService implements Partial { - go = createSpy('go'); + go = vi.fn(); } class MockGlobalMessageService implements Partial { - add = createSpy('add'); + add = vi.fn(); } describe('UnitLevelOrdersViewerGuard', () => { diff --git a/feature-libs/organization/unit-order/core/services/unit-order.service.spec.ts b/feature-libs/organization/unit-order/core/services/unit-order.service.spec.ts index 92da2af1e8e..237ce53249d 100644 --- a/feature-libs/organization/unit-order/core/services/unit-order.service.spec.ts +++ b/feature-libs/organization/unit-order/core/services/unit-order.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { @@ -58,7 +59,7 @@ describe('UnitOrderService', () => { userIdService = TestBed.inject(UserIdService); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); it('should inject UnitOrderService', inject( @@ -119,7 +120,7 @@ describe('UnitOrderService', () => { }); it('should NOT load order list data when user is anonymous', () => { - spyOn(userIdService, 'takeUserId').and.callFake(() => { + vi.spyOn(userIdService, 'takeUserId').mockImplementation(() => { return throwError(() => 'Error'); }); diff --git a/feature-libs/organization/unit-order/core/store/effects/unit-order.effect.spec.ts b/feature-libs/organization/unit-order/core/store/effects/unit-order.effect.spec.ts index 70ef0b5a93d..0e1abcde894 100644 --- a/feature-libs/organization/unit-order/core/store/effects/unit-order.effect.spec.ts +++ b/feature-libs/organization/unit-order/core/store/effects/unit-order.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, provideHttpClient, @@ -64,7 +65,7 @@ describe('Orders effect', () => { describe('loadUnitOrders$', () => { describe('Unit Order History', () => { it('should load unit Orders', () => { - spyOn(orderHistoryConnector, 'getUnitOrderHistory').and.returnValue( + vi.spyOn(orderHistoryConnector, 'getUnitOrderHistory').mockReturnValue( of(mockUserOrders) ); @@ -84,7 +85,7 @@ describe('Orders effect', () => { }); it('should handle failures for load user Orders', () => { - spyOn(orderHistoryConnector, 'getUnitOrderHistory').and.returnValue( + vi.spyOn(orderHistoryConnector, 'getUnitOrderHistory').mockReturnValue( throwError(() => mockError) ); @@ -122,7 +123,7 @@ describe('Orders effect', () => { describe('loadOrderDetails$', () => { it('should load order details', () => { - spyOn(orderHistoryConnector, 'getUnitOrderDetail').and.returnValue( + vi.spyOn(orderHistoryConnector, 'getUnitOrderDetail').mockReturnValue( of(mockOrderDetails) ); const action = new UnitOrderActions.LoadOrderDetails( @@ -144,7 +145,7 @@ describe('Orders effect', () => { mockError, new MockLoggerService() ); - spyOn(orderHistoryConnector, 'getUnitOrderDetail').and.returnValue( + vi.spyOn(orderHistoryConnector, 'getUnitOrderDetail').mockReturnValue( throwError(() => mockError) ); diff --git a/feature-libs/organization/unit-order/core/store/selectors/unit-order.selector.spec.ts b/feature-libs/organization/unit-order/core/store/selectors/unit-order.selector.spec.ts index 2b76eab571e..ed9bfad868b 100644 --- a/feature-libs/organization/unit-order/core/store/selectors/unit-order.selector.spec.ts +++ b/feature-libs/organization/unit-order/core/store/selectors/unit-order.selector.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; import { StateUtils } from '@spartacus/core'; import { OrderHistoryList } from '@spartacus/order/root'; @@ -34,7 +35,7 @@ describe('Unit Level Orders Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('getOrdersLoaderState', () => { @@ -57,7 +58,7 @@ describe('Unit Level Orders Selectors', () => { }); describe('getOrders', () => { - it('should return unit Orders', fakeAsync(() => { + it('should return unit Orders', () => { let result: OrderHistoryList | undefined; store.pipe(select(UnitOrderSelectors.getOrders)).subscribe((value) => { result = value; @@ -66,7 +67,7 @@ describe('Unit Level Orders Selectors', () => { expect(result).toEqual(mockEmptyOrderList); store.dispatch(new UnitOrderActions.LoadUnitOrdersSuccess(mockOrderList)); expect(result).toEqual(mockOrderList); - })); + }); }); describe('getOrdersLoaded', () => { diff --git a/feature-libs/organization/unit-order/occ/adapters/occ-unit-order.adapter.spec.ts b/feature-libs/organization/unit-order/occ/adapters/occ-unit-order.adapter.spec.ts index 6e8f84a0937..17925ba9030 100644 --- a/feature-libs/organization/unit-order/occ/adapters/occ-unit-order.adapter.spec.ts +++ b/feature-libs/organization/unit-order/occ/adapters/occ-unit-order.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpRequest, provideHttpClient, @@ -7,7 +8,7 @@ import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ConverterService, OccEndpointsService } from '@spartacus/core'; import { ORDER_HISTORY_NORMALIZER, @@ -42,9 +43,9 @@ describe('OccUnitOrderAdapter', () => { httpMock = TestBed.inject(HttpTestingController); converter = TestBed.inject(ConverterService); occEnpointsService = TestBed.inject(OccEndpointsService); - spyOn(converter, 'pipeable').and.callThrough(); - spyOn(converter, 'convert').and.callThrough(); - spyOn(occEnpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converter, 'pipeable'); + vi.spyOn(converter, 'convert'); + vi.spyOn(occEnpointsService, 'buildUrl'); }); afterEach(() => { @@ -52,7 +53,7 @@ describe('OccUnitOrderAdapter', () => { }); describe('getUnitLevelOrders', () => { - it('should fetch unit Orders with default options', fakeAsync(() => { + it('should fetch unit Orders with default options', () => { const PAGE_SIZE = 5; occOrderHistoryAdapter .loadUnitOrderHistory(userId, PAGE_SIZE) @@ -67,9 +68,9 @@ describe('OccUnitOrderAdapter', () => { queryParams: { pageSize: PAGE_SIZE.toString() }, } ); - })); + }); - it('should fetch unit Orders with defined options', fakeAsync(() => { + it('should fetch unit Orders with defined options', () => { const PAGE_SIZE = 5; const currentPage = 1; const sort = 'byDate'; @@ -92,7 +93,7 @@ describe('OccUnitOrderAdapter', () => { }, } ); - })); + }); it('should use converter', () => { occOrderHistoryAdapter.loadUnitOrderHistory(userId).subscribe(); @@ -106,7 +107,7 @@ describe('OccUnitOrderAdapter', () => { }); describe('loadUnitOrderDetail', () => { - it('should fetch a single unit-level order', waitForAsync(() => { + it('should fetch a single unit-level order', async () => { occOrderHistoryAdapter .loadUnitOrderDetail(userId, orderDetailCode) .subscribe(); @@ -119,7 +120,7 @@ describe('OccUnitOrderAdapter', () => { urlParams: { userId, orderId: orderDetailCode }, } ); - })); + }); it('should use converter', () => { occOrderHistoryAdapter diff --git a/feature-libs/organization/user-registration/components/form/user-registration-form.component.spec.ts b/feature-libs/organization/user-registration/components/form/user-registration-form.component.spec.ts index 561801e42d1..32092263894 100644 --- a/feature-libs/organization/user-registration/components/form/user-registration-form.component.spec.ts +++ b/feature-libs/organization/user-registration/components/form/user-registration-form.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { DebugElement, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; @@ -133,7 +134,7 @@ describe('UserRegistrationFormComponent', () => { let userRegistrationFormService: UserRegistrationFormService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -172,7 +173,7 @@ describe('UserRegistrationFormComponent', () => { userRegistrationFormService = TestBed.inject(UserRegistrationFormService); msgServcie = TestBed.inject(GlobalMessageService); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(UserRegistrationFormComponent); @@ -186,7 +187,7 @@ describe('UserRegistrationFormComponent', () => { }); it('should initialize registerForm', () => { - spyOnProperty(userRegistrationFormService, 'form', 'get').and.callThrough(); + vi.spyOn(userRegistrationFormService, 'form', 'get'); expect(component.registerForm).toBeInstanceOf(FormGroup); }); @@ -218,7 +219,7 @@ describe('UserRegistrationFormComponent', () => { }); it('should submit form and call the service', () => { - spyOn(userRegistrationFormService, 'registerUser').and.callThrough(); + vi.spyOn(userRegistrationFormService, 'registerUser'); component.registerForm.patchValue({ ...mockOrganizationUser, companyName: 'New Company Inc.', @@ -235,14 +236,14 @@ describe('UserRegistrationFormComponent', () => { }); it('should show error message if service response failed ', () => { - spyOn(userRegistrationFormService, 'registerUser').and.returnValue( + vi.spyOn(userRegistrationFormService, 'registerUser').mockReturnValue( throwError(() => new Error('Simulated error')) ); component.registerForm.patchValue({ ...mockOrganizationUser, companyName: 'New Company Inc.', }); - spyOn(msgServcie, 'add').and.callThrough(); + vi.spyOn(msgServcie, 'add'); component.registerForm.markAllAsTouched(); component.submit(); @@ -256,7 +257,7 @@ describe('UserRegistrationFormComponent', () => { }); it('should not register organization user with invalid form', () => { - spyOn(userRegistrationFormService, 'registerUser').and.callThrough(); + vi.spyOn(userRegistrationFormService, 'registerUser'); component.registerForm.reset(); component.registerForm.patchValue({ firstName: mockOrganizationUser.firstName, diff --git a/feature-libs/organization/user-registration/components/form/user-registration-form.service.spec.ts b/feature-libs/organization/user-registration/components/form/user-registration-form.service.spec.ts index e8c255d7740..bd0cf2a7531 100644 --- a/feature-libs/organization/user-registration/components/form/user-registration-form.service.spec.ts +++ b/feature-libs/organization/user-registration/components/form/user-registration-form.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { @@ -13,7 +14,6 @@ import { import { UserRegisterFacade } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { UserRegistrationFormService } from './user-registration-form.service'; -import createSpy = jasmine.createSpy; class MockGlobalMessageService implements Partial { add() {} @@ -24,15 +24,15 @@ class MockRoutingService implements Partial { } class MockUserAddressService implements Partial { - getDeliveryCountries = createSpy().and.returnValue(of([])); - getRegions = createSpy().and.returnValue(of([])); + getDeliveryCountries = vi.fn().mockReturnValue(of([])); + getRegions = vi.fn().mockReturnValue(of([])); loadDeliveryCountries(): void { return; } } class MockUserRegisterFacade implements Partial { - getTitles = createSpy().and.returnValue(of([])); + getTitles = vi.fn().mockReturnValue(of([])); } class MockTranslationService implements Partial { @@ -179,7 +179,7 @@ describe('UserRegistrationFormService', () => { }); it('should redirect to login page', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.registerUser(service.form).subscribe().unsubscribe(); diff --git a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts index bb47493172d..83272b1cb1e 100644 --- a/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts +++ b/feature-libs/organization/user-registration/components/registration-otp-form/user-registration-otp-form.component.spec.ts @@ -1,6 +1,7 @@ +import { vi } from 'vitest'; import { HttpErrorResponse } from '@angular/common/http'; import { Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -24,10 +25,9 @@ import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feat import { Observable, of, throwError } from 'rxjs'; import { UserRegistrationFormService } from '../form'; import { UserRegistrationOTPFormComponent } from './user-registration-otp-form.component'; -import createSpy = jasmine.createSpy; class MockRoutingService { - go = createSpy(); + go = vi.fn(); } class MockGlobalMessageService implements Partial { @@ -116,7 +116,7 @@ describe('UserRegistrationOTPFormComponent', () => { let fixture: ComponentFixture; let verificationTokenFacade: VerificationTokenFacade; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -140,7 +140,7 @@ describe('UserRegistrationOTPFormComponent', () => { ], }); verificationTokenFacade = TestBed.inject(VerificationTokenFacade); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(UserRegistrationOTPFormComponent); @@ -158,7 +158,10 @@ describe('UserRegistrationOTPFormComponent', () => { }); it('should not submit if form is invalid', () => { - spyOn(verificationTokenFacade, 'createVerificationToken').and.returnValue( + vi.spyOn( + verificationTokenFacade, + 'createVerificationToken' + ).mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -186,7 +189,10 @@ describe('UserRegistrationOTPFormComponent', () => { }); it('should submit form when valid', () => { - spyOn(verificationTokenFacade, 'createVerificationToken').and.returnValue( + vi.spyOn( + verificationTokenFacade, + 'createVerificationToken' + ).mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -218,13 +224,16 @@ describe('UserRegistrationOTPFormComponent', () => { }); it('should mark all fields as touched if form is invalid', () => { - spyOn(verificationTokenFacade, 'createVerificationToken').and.returnValue( + vi.spyOn( + verificationTokenFacade, + 'createVerificationToken' + ).mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', }) ); - spyOn(component.registerForm, 'markAllAsTouched').and.callThrough(); + vi.spyOn(component.registerForm, 'markAllAsTouched'); component.registerForm.patchValue({ email: '', }); @@ -274,9 +283,10 @@ describe('UserRegistrationOTPFormComponent', () => { status: 400, url: 'https://localhost:9002/occ/v2/electronics-spa/users/anonymous/verificationToken?lang=en&curr=USD', }); - spyOn(verificationTokenFacade, 'createVerificationToken').and.returnValue( - throwError(() => httpErrorResponse) - ); + vi.spyOn( + verificationTokenFacade, + 'createVerificationToken' + ).mockReturnValue(throwError(() => httpErrorResponse)); component.onSubmit(); expect(routingService.go).toHaveBeenCalled(); diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts index f0ceddfd2d3..6338f74ea73 100644 --- a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form-component.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { FormBuilder, UntypedFormGroup } from '@angular/forms'; import { @@ -92,7 +93,7 @@ describe('RegisterVerificationTokenFormComponentService', () => { }); it('should redirect to login page', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); service.registerUser(service.form.value).subscribe().unsubscribe(); @@ -102,7 +103,7 @@ describe('RegisterVerificationTokenFormComponentService', () => { }); it('should display a success message after registration', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.registerUser(service.form.value).subscribe().unsubscribe(); @@ -117,7 +118,7 @@ describe('RegisterVerificationTokenFormComponentService', () => { }); it('should call buildMessageContent and translate correctly', () => { - spyOn(translationService, 'translate').and.callThrough(); + vi.spyOn(translationService, 'translate'); const formValue = { phoneNumber: '123456789', @@ -153,7 +154,7 @@ describe('RegisterVerificationTokenFormComponentService', () => { }); it('should call registerUser with correct data', () => { - spyOn(userRegistrationFacade, 'registerUser').and.callThrough(); + vi.spyOn(userRegistrationFacade, 'registerUser'); service.form.setValue({ tokenId: 'testTokenId', diff --git a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts index c7d5afd048d..3a7a8bcc82f 100644 --- a/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts +++ b/feature-libs/organization/user-registration/components/verification-token-form/verification-token-form.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { ChangeDetectorRef, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -27,7 +28,6 @@ import { BehaviorSubject, of } from 'rxjs'; import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-registration-constants'; import { RegisterVerificationTokenFormComponentService } from './verification-token-form-component.service'; import { RegisterVerificationTokenFormComponent } from './verification-token-form.component'; -import createSpy = jasmine.createSpy; const isBusySubject = new BehaviorSubject(false); class MockFormComponentService @@ -37,23 +37,23 @@ class MockFormComponentService tokenId: new UntypedFormControl(), tokenCode: new UntypedFormControl(), }); - login = createSpy().and.stub(); - createVerificationToken = createSpy().and.returnValue( - of({ tokenId: 'testTokenId', expiresIn: '300' }) - ); - displayMessage = createSpy('displayMessage').and.stub(); + login = vi.fn().mockImplementation(() => {}); + createVerificationToken = vi + .fn() + .mockReturnValue(of({ tokenId: 'testTokenId', expiresIn: '300' })); + displayMessage = vi.fn().mockImplementation(() => {}); } class MockRoutingService { - go = createSpy(); + go = vi.fn(); } class MockStore { - dispatch = jasmine.createSpy(); - select = jasmine.createSpy().and.returnValue(of({})); + dispatch = vi.fn(); + select = vi.fn().mockReturnValue(of({})); } class MockLaunchDialogService implements Partial { - openDialogAndSubscribe = createSpy().and.stub(); + openDialogAndSubscribe = vi.fn().mockImplementation(() => {}); } describe('RegisterVerificationTokenFormComponent', () => { @@ -65,7 +65,7 @@ describe('RegisterVerificationTokenFormComponent', () => { let launchDialogService: LaunchDialogService; let routineservice: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -97,7 +97,7 @@ describe('RegisterVerificationTokenFormComponent', () => { add: { imports: [MockTranslatePipe, MockUrlPipe] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(RegisterVerificationTokenFormComponent); @@ -157,7 +157,7 @@ describe('RegisterVerificationTokenFormComponent', () => { describe('refresh with no tokenId/loginId', () => { it('should navigate back to login page', () => { - spyOn(service, 'displayMessage'); + vi.spyOn(service, 'displayMessage'); history.pushState( { tokenId: '', @@ -176,14 +176,14 @@ describe('RegisterVerificationTokenFormComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); }); it('should call the service method on submit', () => { - spyOn(service, 'registerUser').and.returnValue( + vi.spyOn(service, 'registerUser').mockReturnValue( of({ email: 'test@example.com', firstName: 'John', @@ -216,9 +216,9 @@ describe('RegisterVerificationTokenFormComponent', () => { it('should resend OTP', () => { component.target = 'example@example.com'; - spyOn(component, 'startWaitTimeInterval'); - spyOn(service, 'displayMessage'); - spyOn(facade, 'createVerificationToken').and.returnValue( + vi.spyOn(component, 'startWaitTimeInterval'); + vi.spyOn(service, 'displayMessage'); + vi.spyOn(facade, 'createVerificationToken').mockReturnValue( of({ tokenId: 'tokenId', expiresIn: '300', diff --git a/feature-libs/organization/user-registration/core/connectors/user-registration.connector.spec.ts b/feature-libs/organization/user-registration/core/connectors/user-registration.connector.spec.ts index a8d50a96cca..6f15a5843b0 100644 --- a/feature-libs/organization/user-registration/core/connectors/user-registration.connector.spec.ts +++ b/feature-libs/organization/user-registration/core/connectors/user-registration.connector.spec.ts @@ -1,11 +1,10 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { OrganizationUserRegistration } from '@spartacus/organization/user-registration/root'; import { of } from 'rxjs'; import { UserRegistrationAdapter } from './user-registration.adapter'; import { UserRegistrationConnector } from './user-registration.connector'; -import createSpy = jasmine.createSpy; - const userData: OrganizationUserRegistration = { titleCode: 'Mr', firstName: 'John', @@ -15,9 +14,7 @@ const userData: OrganizationUserRegistration = { }; class MockUserRegistrationAdapter implements UserRegistrationAdapter { - registerUser = createSpy( - 'UserRegistrationAdapter.registerUser' - ).and.returnValue(of(userData)); + registerUser = vi.fn().mockReturnValue(of(userData)); } describe('UserRegistrationConnector', () => { diff --git a/feature-libs/organization/user-registration/core/facade/user-registration.service.spec.ts b/feature-libs/organization/user-registration/core/facade/user-registration.service.spec.ts index f2dc79d3d6b..c606e9a3708 100644 --- a/feature-libs/organization/user-registration/core/facade/user-registration.service.spec.ts +++ b/feature-libs/organization/user-registration/core/facade/user-registration.service.spec.ts @@ -1,11 +1,10 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { OrganizationUserRegistration } from '@spartacus/organization/user-registration/root'; import { UserRegistrationConnector } from '../connectors'; import { UserRegistrationService } from './user-registration.service'; -import createSpy = jasmine.createSpy; - const mockOrganizationUser: OrganizationUserRegistration = { titleCode: 'Mr.', firstName: 'John', @@ -17,10 +16,11 @@ const mockOrganizationUser: OrganizationUserRegistration = { class MockUserRegistrationConnector implements Partial { - registerUser = createSpy().and.callFake( - (mockOrganizationUser: OrganizationUserRegistration) => + registerUser = vi + .fn() + .mockImplementation((mockOrganizationUser: OrganizationUserRegistration) => of(mockOrganizationUser) - ); + ); } describe('UserRegistrationService', () => { diff --git a/feature-libs/organization/user-registration/core/http-interceptors/conflict/conflict.handler.spec.ts b/feature-libs/organization/user-registration/core/http-interceptors/conflict/conflict.handler.spec.ts index ac7054b60a5..1b46214fa26 100644 --- a/feature-libs/organization/user-registration/core/http-interceptors/conflict/conflict.handler.spec.ts +++ b/feature-libs/organization/user-registration/core/http-interceptors/conflict/conflict.handler.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { OrganizationUserRegistrationConflictHandler } from './conflict.handler'; import { HttpErrorResponse, HttpRequest } from '@angular/common/http'; @@ -51,7 +52,7 @@ describe('OrganizationUserRegistrationConflictHandler', () => { }); it('should handle existing organization user conflict', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockOrganizationUserConflictResponse); expect(globalMessageService.add).toHaveBeenCalledWith( diff --git a/feature-libs/organization/user-registration/occ/adapters/occ-user-registration.adapter.spec.ts b/feature-libs/organization/user-registration/occ/adapters/occ-user-registration.adapter.spec.ts index 6093484a318..0ee7041d1e2 100644 --- a/feature-libs/organization/user-registration/occ/adapters/occ-user-registration.adapter.spec.ts +++ b/feature-libs/organization/user-registration/occ/adapters/occ-user-registration.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -85,8 +86,8 @@ describe('OccUserRegistrationAdapter', () => { httpMock = TestBed.inject(HttpTestingController); converter = TestBed.inject(ConverterService); occEndpointsService = TestBed.inject(OccEndpointsService); - spyOn(converter, 'convert').and.callThrough(); - spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converter, 'convert'); + vi.spyOn(occEndpointsService, 'buildUrl'); }); afterEach(() => { diff --git a/feature-libs/organization/vitest.config.ts b/feature-libs/organization/vitest.config.ts new file mode 100644 index 00000000000..aa116d92df9 --- /dev/null +++ b/feature-libs/organization/vitest.config.ts @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/core/src/process/store/reducers/index': `${root}/core-libs/core/src/process/store/reducers/index.ts`, + 'core-libs/core/src/global-message/models/global-message.model': `${root}/core-libs/core/src/global-message/models/global-message.model.ts`, + 'core-libs/storefront/cms-components/misc/icon/testing/icon-testing.module': `${root}/core-libs/storefront/cms-components/misc/icon/testing/icon-testing.module.ts`, + 'core-libs/storefront/layout/a11y/keyboard-focus/focus-testing.module': `${root}/core-libs/storefront/layout/a11y/keyboard-focus/focus-testing.module.ts`, + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + 'core-libs/core/src/occ/adapters/user/unit-test.helper': `${root}/core-libs/core/src/occ/adapters/user/unit-test.helper.ts`, + 'core-libs/core/src/i18n/testing/mock-translation.service': `${root}/core-libs/core/src/i18n/testing/mock-translation.service.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe.ts`, + 'core-libs/storefront/shared/components/list-navigation/pagination/testing/pagination-testing.module': `${root}/core-libs/storefront/shared/components/list-navigation/pagination/testing/pagination-testing.module.ts`, + 'core-libs/storefront/shared/components/split-view/testing/spit-view-testing.module': `${root}/core-libs/storefront/shared/components/split-view/testing/spit-view-testing.module.ts`, + 'core-libs/core/src/util/testing-time-utils': `${root}/core-libs/core/src/util/testing-time-utils.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/url.pipe': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/url.pipe.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module.ts`, + 'core-libs/storefront/shared/components/split-view/view/view.component': `${root}/core-libs/storefront/shared/components/split-view/view/view.component.ts`, + 'core-libs/core/src/features-config/feature-toggles/testing': `${root}/core-libs/core/src/features-config/feature-toggles/testing/index.ts`, + }, + }, + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/organization`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-organization.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/pdf-invoices/components/invoices-list/invoices-list.component.spec.ts b/feature-libs/pdf-invoices/components/invoices-list/invoices-list.component.spec.ts index 3a24f06a2ca..5512adb9d9a 100644 --- a/feature-libs/pdf-invoices/components/invoices-list/invoices-list.component.spec.ts +++ b/feature-libs/pdf-invoices/components/invoices-list/invoices-list.component.spec.ts @@ -26,10 +26,9 @@ import { SortingComponent, } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; -import { EMPTY, Observable, of, throwError } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { EMPTY, Observable, firstValueFrom, of, throwError } from 'rxjs'; +import { vi } from 'vitest'; import { InvoicesListComponent } from './invoices-list.component'; -import createSpy = jasmine.createSpy; const blob = new Blob(); @@ -156,7 +155,7 @@ class MockPDFInvoicesFacade implements Partial { } class MockFileDownloadService { - download = createSpy('MockFileDownloadService.download Spy'); + download = vi.fn(); } class MockLanguageService { @@ -232,8 +231,8 @@ describe('InvoicesListComponent', () => { downloadService = TestBed.inject(FileDownloadService); globalMessageService = TestBed.inject(GlobalMessageService); - spyOn(globalMessageService, 'add').and.callThrough(); - spyOn(translationService, 'translate').and.returnValue(of('test')); + vi.spyOn(globalMessageService, 'add'); + vi.spyOn(translationService, 'translate').mockReturnValue(of('test')); }); beforeEach(() => { @@ -247,10 +246,10 @@ describe('InvoicesListComponent', () => { }); it('should show feature not enabled error when the API returns error', () => { - spyOn(pdfInvoicesFacade, 'getInvoicesForOrder').and.returnValue( + vi.spyOn(pdfInvoicesFacade, 'getInvoicesForOrder').mockReturnValue( throwError(mockInvoicesNotEnabledError) ); - spyOn(component, 'getNotEnabledError').and.callThrough(); + vi.spyOn(component, 'getNotEnabledError'); // Change the page const newPage = 3; @@ -262,14 +261,8 @@ describe('InvoicesListComponent', () => { ); }); - it('should read document list', (done) => { - let orderInvoiceList: OrderInvoiceList = {}; - let queryParams: InvoiceQueryParams = {}; - component.invoicesList$ - .pipe(take(1)) - .subscribe((value: OrderInvoiceList) => { - orderInvoiceList = value; - }); + it('should read document list', async () => { + const orderInvoiceList = await firstValueFrom(component.invoicesList$); expect(orderInvoiceList).toEqual(mockOrderInvoiceList); expect(component.pagination).toEqual({ currentPage: 0, @@ -278,12 +271,7 @@ describe('InvoicesListComponent', () => { totalResults: 16, sort: 'invoiceId:asc', }); - component.queryParams$ - .pipe(take(1)) - .subscribe((value: InvoiceQueryParams) => { - queryParams = value; - done(); - }); + const queryParams = await firstValueFrom(component.queryParams$); expect(queryParams).toEqual({ currentPage: 0, pageSize: 5, @@ -294,8 +282,8 @@ describe('InvoicesListComponent', () => { it('Should change page and invoke the API', () => { // Spy functions to ensure new invoices are being fetched - spyOn(component, 'updateQueryParams').and.callThrough(); - spyOn(pdfInvoicesFacade, 'getInvoicesForOrder').and.callThrough(); + vi.spyOn(component, 'updateQueryParams'); + vi.spyOn(pdfInvoicesFacade, 'getInvoicesForOrder'); // By default currentPage will be 0 expect(component._initQueryParams.currentPage).toEqual(0); @@ -318,8 +306,8 @@ describe('InvoicesListComponent', () => { it('Should change sort and invoke the API', () => { // Spy functions to ensure new invoices are being fetched - spyOn(component, 'updateQueryParams').and.callThrough(); - spyOn(pdfInvoicesFacade, 'getInvoicesForOrder').and.callThrough(); + vi.spyOn(component, 'updateQueryParams'); + vi.spyOn(pdfInvoicesFacade, 'getInvoicesForOrder'); // Getting ready to change sort, make sure that current sort is different and should be one of the sortOptions key const newSortCode = 'byTotalAmountAsc'; expect(component._initQueryParams.sort).not.toEqual(newSortCode); @@ -365,16 +353,16 @@ describe('InvoicesListComponent', () => { const tableHeaders = tableElement.queryAll(By.css('th')); expect(tableHeaders?.length).toEqual(5); - expect(tableHeaders[0].properties.innerText).toEqual( + expect(tableHeaders[0].nativeElement.textContent.trim()).toEqual( 'pdfInvoices.invoicesTable.invoiceId' ); - expect(tableHeaders[1].properties.innerText).toEqual( + expect(tableHeaders[1].nativeElement.textContent.trim()).toEqual( 'pdfInvoices.invoicesTable.createdAt' ); - expect(tableHeaders[2].properties.innerText).toEqual( + expect(tableHeaders[2].nativeElement.textContent.trim()).toEqual( 'pdfInvoices.invoicesTable.netAmount' ); - expect(tableHeaders[3].properties.innerText).toEqual( + expect(tableHeaders[3].nativeElement.textContent.trim()).toEqual( 'pdfInvoices.invoicesTable.totalAmount' ); expect(tableHeaders[4].children[0].attributes.title).toEqual( @@ -398,36 +386,36 @@ describe('InvoicesListComponent', () => { expect(tableCells?.length).toEqual(5); - expect(tableCells[0].nativeElement.innerText).toEqual( + expect(tableCells[0].nativeElement.textContent.trim()).toEqual( mockOrderInvoiceList.invoices?.[rowNumber]?.invoiceId ); - expect(isDate(tableCells[1].nativeElement.innerText)).toEqual( + expect(isDate(tableCells[1].nativeElement.textContent.trim())).toEqual( !!mockOrderInvoiceList.invoices?.[rowNumber]?.createdAt ); if ( mockOrderInvoiceList.invoices?.[rowNumber]?.netAmount?.formattedValue ) { - expect(tableCells[2].nativeElement.innerText).toEqual( + expect(tableCells[2].nativeElement.textContent.trim()).toEqual( mockOrderInvoiceList.invoices?.[rowNumber]?.netAmount?.formattedValue ); } else { - expect(tableCells[2].nativeElement.innerHTML).toEqual( - ` ${mockOrderInvoiceList.invoices?.[rowNumber]?.netAmount?.currencyIso} ${mockOrderInvoiceList.invoices?.[rowNumber]?.netAmount?.value} ` + expect(tableCells[2].nativeElement.textContent.trim()).toEqual( + `${mockOrderInvoiceList.invoices?.[rowNumber]?.netAmount?.currencyIso} ${mockOrderInvoiceList.invoices?.[rowNumber]?.netAmount?.value}` ); } if ( mockOrderInvoiceList.invoices?.[rowNumber]?.totalAmount?.formattedValue ) { - expect(tableCells[3].nativeElement.innerText).toEqual( + expect(tableCells[3].nativeElement.textContent.trim()).toEqual( mockOrderInvoiceList.invoices?.[rowNumber]?.totalAmount ?.formattedValue ); } else { - expect(tableCells[3].nativeElement.innerHTML).toEqual( - ` ${mockOrderInvoiceList.invoices?.[rowNumber]?.totalAmount?.currencyIso} ${mockOrderInvoiceList.invoices?.[rowNumber]?.totalAmount?.value} ` + expect(tableCells[3].nativeElement.textContent.trim()).toEqual( + `${mockOrderInvoiceList.invoices?.[rowNumber]?.totalAmount?.currencyIso} ${mockOrderInvoiceList.invoices?.[rowNumber]?.totalAmount?.value}` ); } @@ -444,9 +432,9 @@ describe('InvoicesListComponent', () => { externalSystemId: '', }; - spyOn(pdfInvoicesFacade, 'getInvoicePDF').and.returnValue(of(blob)); + vi.spyOn(pdfInvoicesFacade, 'getInvoicePDF').mockReturnValue(of(blob)); const fakeUrl = 'blob:http://localhost:4321/15-09-2023-1234'; - spyOn(URL, 'createObjectURL').and.returnValue(fakeUrl); + vi.spyOn(URL, 'createObjectURL').mockReturnValue(fakeUrl); expect(invoicePDF).not.toBeUndefined(); component.downloadPDFInvoice( @@ -473,9 +461,9 @@ describe('InvoicesListComponent', () => { externalSystemId: '', }; - spyOn(pdfInvoicesFacade, 'getInvoicePDF').and.returnValue(of(blob)); + vi.spyOn(pdfInvoicesFacade, 'getInvoicePDF').mockReturnValue(of(blob)); const fakeUrl = 'blob:http://localhost:4321/15-09-2023-1234'; - spyOn(URL, 'createObjectURL').and.returnValue(fakeUrl); + vi.spyOn(URL, 'createObjectURL').mockReturnValue(fakeUrl); component.downloadPDFInvoice( invoicePDF.invoiceId || '', diff --git a/feature-libs/pdf-invoices/core/connectors/pdf-invoices.connector.spec.ts b/feature-libs/pdf-invoices/core/connectors/pdf-invoices.connector.spec.ts index 33dca995e83..d520ba52e8e 100644 --- a/feature-libs/pdf-invoices/core/connectors/pdf-invoices.connector.spec.ts +++ b/feature-libs/pdf-invoices/core/connectors/pdf-invoices.connector.spec.ts @@ -3,13 +3,11 @@ import { InvoiceQueryParams, InvoicesFields, } from '@spartacus/pdf-invoices/root'; -import { of } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { firstValueFrom, of } from 'rxjs'; +import { vi } from 'vitest'; import { PDFInvoicesAdapter } from './pdf-invoices.adapter'; import { PDFInvoicesConnector } from './pdf-invoices.connector'; -import createSpy = jasmine.createSpy; - const mockUserId = 'userId1'; const mockOrderId = '15092023'; const mockExternalSystemId = 'IMPERIAL'; @@ -22,20 +20,22 @@ const mockInvoiceQueryParams: InvoiceQueryParams = { }; class MockPDFInvoicesAdapter implements Partial { - getInvoicesForOrder = createSpy( - 'PDFInvoicesAdapter.getInvoicesForOrder' - ).and.callFake( - (_userId: string, _orderId: string, _queryParams: InvoiceQueryParams) => - of({}) - ); - getInvoicePDF = createSpy('PDFInvoicesAdapter.getInvoicePDF').and.callFake( - ( - _userId: string, - _orderId: string, - _invoiceId: string, - _externalSystemId?: string - ) => of({}) - ); + getInvoicesForOrder = vi + .fn() + .mockImplementation( + (_userId: string, _orderId: string, _queryParams: InvoiceQueryParams) => + of({}) + ); + getInvoicePDF = vi + .fn() + .mockImplementation( + ( + _userId: string, + _orderId: string, + _invoiceId: string, + _externalSystemId?: string + ) => of({}) + ); } describe('PDFInvoicesConnector', () => { @@ -61,16 +61,15 @@ describe('PDFInvoicesConnector', () => { expect(pdfInvoicesConnector).toBeTruthy(); }); - it('should call adapter when getInvoicesForOrder is invoked', (done) => { - let result; - pdfInvoicesConnector - .getInvoicesForOrder(mockUserId, mockOrderId, mockInvoiceQueryParams) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual({}); - done(); - }); + it('should call adapter when getInvoicesForOrder is invoked', async () => { + const result = await firstValueFrom( + pdfInvoicesConnector.getInvoicesForOrder( + mockUserId, + mockOrderId, + mockInvoiceQueryParams + ) + ); + expect(result).toEqual({}); expect(adapter.getInvoicesForOrder).toHaveBeenCalledWith( mockUserId, mockOrderId, @@ -78,21 +77,16 @@ describe('PDFInvoicesConnector', () => { ); }); - it('should call adapter when getInvoicePDF is invoked', (done) => { - let result; - pdfInvoicesConnector - .getInvoicePDF( + it('should call adapter when getInvoicePDF is invoked', async () => { + const result = await firstValueFrom( + pdfInvoicesConnector.getInvoicePDF( mockUserId, mockOrderId, mockInvoiceId, mockExternalSystemId ) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual({}); - done(); - }); + ); + expect(result).toEqual({}); expect(adapter.getInvoicePDF).toHaveBeenCalledWith( mockUserId, mockOrderId, @@ -101,16 +95,11 @@ describe('PDFInvoicesConnector', () => { ); }); - it('should call adapter when getInvoicePDF is invoked without externalSystemId', (done) => { - let result; - pdfInvoicesConnector - .getInvoicePDF(mockUserId, mockOrderId, mockInvoiceId) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual({}); - done(); - }); + it('should call adapter when getInvoicePDF is invoked without externalSystemId', async () => { + const result = await firstValueFrom( + pdfInvoicesConnector.getInvoicePDF(mockUserId, mockOrderId, mockInvoiceId) + ); + expect(result).toEqual({}); expect(adapter.getInvoicePDF).toHaveBeenCalledWith( mockUserId, mockOrderId, diff --git a/feature-libs/pdf-invoices/core/http-interceptors/bad-request/pdf-invoices-badrequest.handler.spec.ts b/feature-libs/pdf-invoices/core/http-interceptors/bad-request/pdf-invoices-badrequest.handler.spec.ts index dcb34f903fa..370bfc38fa6 100644 --- a/feature-libs/pdf-invoices/core/http-interceptors/bad-request/pdf-invoices-badrequest.handler.spec.ts +++ b/feature-libs/pdf-invoices/core/http-interceptors/bad-request/pdf-invoices-badrequest.handler.spec.ts @@ -5,6 +5,7 @@ import { GlobalMessageType, HttpResponseStatus, } from '@spartacus/core'; +import { vi } from 'vitest'; import { PDFInvoicesBadRequestHandler } from './pdf-invoices-badrequest.handler'; class MockGlobalMessageService { @@ -65,7 +66,7 @@ describe('PDFInvoicesDateBadRequestHandler', () => { }); it('should handle invalid order id bad request', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); pdfInvoicesBRHandler.handleError( MockRequest, MockNoOrderIdBadRequestResponse @@ -80,7 +81,7 @@ describe('PDFInvoicesDateBadRequestHandler', () => { }); it('should handle invoice download bad request', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); pdfInvoicesBRHandler.handleError( MockRequest, MockDownloadPDFBadRequestResponse diff --git a/feature-libs/pdf-invoices/core/services/pdf-invoices.service.spec.ts b/feature-libs/pdf-invoices/core/services/pdf-invoices.service.spec.ts index 4901d71386a..e70217c03c9 100644 --- a/feature-libs/pdf-invoices/core/services/pdf-invoices.service.spec.ts +++ b/feature-libs/pdf-invoices/core/services/pdf-invoices.service.spec.ts @@ -4,13 +4,11 @@ import { InvoiceQueryParams, InvoicesFields, } from '@spartacus/pdf-invoices/root'; -import { of } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { firstValueFrom, of } from 'rxjs'; +import { vi } from 'vitest'; import { PDFInvoicesConnector } from '../connectors/pdf-invoices.connector'; import { PDFInvoicesService } from './pdf-invoices.service'; -import createSpy = jasmine.createSpy; - const mockUserId = 'userId1'; const mockOrderId = '15092023'; const mockExternalSystemId = 'IMPERIAL'; @@ -25,30 +23,30 @@ const mockInvoiceQueryParams: InvoiceQueryParams = { const blob = new Blob(); class MockPDFInvoicesConnector implements Partial { - getInvoicesForOrder = createSpy( - 'PDFInvoicesConnector.getInvoicesForOrder' - ).and.callFake( - (_userId: string, _orderId: string, _queryParams: InvoiceQueryParams) => - of({}) - ); - getInvoicePDF = createSpy('PDFInvoicesConnector.getInvoicePDF').and.callFake( - ( - _userId: string, - _orderId: string, - _invoiceId: string, - _externalSystemId?: string - ) => of(blob) - ); + getInvoicesForOrder = vi + .fn() + .mockImplementation( + (_userId: string, _orderId: string, _queryParams: InvoiceQueryParams) => + of({}) + ); + getInvoicePDF = vi + .fn() + .mockImplementation( + ( + _userId: string, + _orderId: string, + _invoiceId: string, + _externalSystemId?: string + ) => of(blob) + ); } class MockUserIdService implements Partial { - takeUserId = createSpy('UserIdService.takeUserId').and.returnValue( - of(mockUserId) - ); + takeUserId = vi.fn().mockReturnValue(of(mockUserId)); } class MockRoutingService implements Partial { - getRouterState = createSpy('RoutingService.getRouterState').and.returnValue( + getRouterState = vi.fn().mockReturnValue( of({ state: { semanticRoute: 'orders', @@ -90,16 +88,15 @@ describe('PDFInvoicesService', () => { expect(pdfInvoicesService).toBeTruthy(); }); - it('should call connector when getInvoicesForOrder is invoked', (done) => { - let result; - pdfInvoicesService - .getInvoicesForOrder(mockInvoiceQueryParams, mockUserId, mockOrderId) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual({}); - done(); - }); + it('should call connector when getInvoicesForOrder is invoked', async () => { + const result = await firstValueFrom( + pdfInvoicesService.getInvoicesForOrder( + mockInvoiceQueryParams, + mockUserId, + mockOrderId + ) + ); + expect(result).toEqual({}); expect(connector.getInvoicesForOrder).toHaveBeenCalledWith( mockUserId, mockOrderId, @@ -107,16 +104,11 @@ describe('PDFInvoicesService', () => { ); }); - it('should set userId, orderId and call connector when getInvoicesForOrder is invoked without userId and orderId', (done) => { - let result; - pdfInvoicesService - .getInvoicesForOrder(mockInvoiceQueryParams) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual({}); - done(); - }); + it('should set userId, orderId and call connector when getInvoicesForOrder is invoked without userId and orderId', async () => { + const result = await firstValueFrom( + pdfInvoicesService.getInvoicesForOrder(mockInvoiceQueryParams) + ); + expect(result).toEqual({}); expect(connector.getInvoicesForOrder).toHaveBeenCalledWith( mockUserId, mockOrderId, @@ -124,21 +116,16 @@ describe('PDFInvoicesService', () => { ); }); - it('should call connector when getInvoicePDF is invoked', (done) => { - let result; - pdfInvoicesService - .getInvoicePDF( + it('should call connector when getInvoicePDF is invoked', async () => { + const result = await firstValueFrom( + pdfInvoicesService.getInvoicePDF( mockInvoiceId, mockExternalSystemId, mockUserId, mockOrderId ) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual(blob); - done(); - }); + ); + expect(result).toEqual(blob); expect(connector.getInvoicePDF).toHaveBeenCalledWith( mockUserId, mockOrderId, @@ -147,16 +134,16 @@ describe('PDFInvoicesService', () => { ); }); - it('should call connector when getInvoicePDF is invoked without externalSystemId', (done) => { - let result; - pdfInvoicesService - .getInvoicePDF(mockInvoiceId, undefined, mockUserId, mockOrderId) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual(blob); - done(); - }); + it('should call connector when getInvoicePDF is invoked without externalSystemId', async () => { + const result = await firstValueFrom( + pdfInvoicesService.getInvoicePDF( + mockInvoiceId, + undefined, + mockUserId, + mockOrderId + ) + ); + expect(result).toEqual(blob); expect(connector.getInvoicePDF).toHaveBeenCalledWith( mockUserId, mockOrderId, @@ -165,16 +152,11 @@ describe('PDFInvoicesService', () => { ); }); - it('should set userId, orderId and call connector when getInvoicePDF is invoked without userId, orderId, externalSystemId', (done) => { - let result; - pdfInvoicesService - .getInvoicePDF(mockInvoiceId) - .pipe(take(1)) - .subscribe((res: any) => { - result = res; - expect(result).toEqual(blob); - done(); - }); + it('should set userId, orderId and call connector when getInvoicePDF is invoked without userId, orderId, externalSystemId', async () => { + const result = await firstValueFrom( + pdfInvoicesService.getInvoicePDF(mockInvoiceId) + ); + expect(result).toEqual(blob); expect(connector.getInvoicePDF).toHaveBeenCalledWith( mockUserId, mockOrderId, diff --git a/feature-libs/pdf-invoices/karma.conf.js b/feature-libs/pdf-invoices/karma.conf.js deleted file mode 100644 index d5202415569..00000000000 --- a/feature-libs/pdf-invoices/karma.conf.js +++ /dev/null @@ -1,50 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - ], - parallelOptions: { - executors: 2, - shardStrategy: 'round-robin', - }, - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots'], - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/pdf-invoices'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 75, - functions: 85, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/pdf-invoices/occ/adapters/occ-pdf-invoices.adapter.spec.ts b/feature-libs/pdf-invoices/occ/adapters/occ-pdf-invoices.adapter.spec.ts index 3c4e46b13fb..f2cc4eebef7 100644 --- a/feature-libs/pdf-invoices/occ/adapters/occ-pdf-invoices.adapter.spec.ts +++ b/feature-libs/pdf-invoices/occ/adapters/occ-pdf-invoices.adapter.spec.ts @@ -21,8 +21,8 @@ import { InvoicesFields, OrderInvoiceList, } from '@spartacus/pdf-invoices/root'; -import { throwError } from 'rxjs'; -import { take } from 'rxjs/operators'; +import { firstValueFrom, throwError } from 'rxjs'; +import { vi } from 'vitest'; import { OccPDFInvoicesAdapter } from './occ-pdf-invoices.adapter'; const mockUserId = 'userId1'; @@ -131,14 +131,14 @@ describe('OccPDFInvoicesAdapter', () => { }); describe(`get invoices for an order`, () => { - it(`should show PDF Invoices for given user id, order id`, (done) => { - occPDFInvoicesAdapter - .getInvoicesForOrder(mockUserId, mockOrderId, mockInvoiceQueryParams) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockInvoicesList); - done(); - }); + it(`should show PDF Invoices for given user id, order id`, async () => { + const resultPromise = firstValueFrom( + occPDFInvoicesAdapter.getInvoicesForOrder( + mockUserId, + mockOrderId, + mockInvoiceQueryParams + ) + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -157,23 +157,26 @@ describe('OccPDFInvoicesAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); mockReq.flush(mockInvoicesList); expect(mockReq.request.responseType).toEqual('json'); + + const result = await resultPromise; + expect(result).toEqual(mockInvoicesList); }); - it(`should result in error when Error is thrown`, (done) => { - spyOn(httpClient, 'get').and.returnValue( + it(`should result in error when Error is thrown`, async () => { + vi.spyOn(httpClient, 'get').mockReturnValue( throwError(mockNoOrderIdBadRequestResponse) ); let result: HttpErrorModel | undefined; - const subscription = occPDFInvoicesAdapter - .getInvoicesForOrder(mockUserId, mockOrderId, mockInvoiceQueryParams) - .pipe(take(1)) - .subscribe({ - error: (err: any) => { - result = err; - done(); - }, - }); + await firstValueFrom( + occPDFInvoicesAdapter.getInvoicesForOrder( + mockUserId, + mockOrderId, + mockInvoiceQueryParams + ) + ).catch((err: any) => { + result = err; + }); expect(result).toEqual( tryNormalizeHttpError( @@ -181,8 +184,6 @@ describe('OccPDFInvoicesAdapter', () => { new MockLoggerService() ) ); - - subscription.unsubscribe(); }); }); @@ -190,14 +191,14 @@ describe('OccPDFInvoicesAdapter', () => { const mockFile: File = new File([], 'MockOrderInvoice', { type: 'application/pdf', }); - it(`should download PDF Invoices for given user id, order id`, (done) => { - occPDFInvoicesAdapter - .getInvoicePDF(mockUserId, mockOrderId, mockInvoiceId) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockFile); - done(); - }); + it(`should download PDF Invoices for given user id, order id`, async () => { + const resultPromise = firstValueFrom( + occPDFInvoicesAdapter.getInvoicePDF( + mockUserId, + mockOrderId, + mockInvoiceId + ) + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -210,21 +211,20 @@ describe('OccPDFInvoicesAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('blob'); mockReq.flush(mockFile); + + const result = await resultPromise; + expect(result).toEqual(mockFile); }); - it(`should download PDF Invoices for given user id, order id and external system id`, (done) => { - occPDFInvoicesAdapter - .getInvoicePDF( + it(`should download PDF Invoices for given user id, order id and external system id`, async () => { + const resultPromise = firstValueFrom( + occPDFInvoicesAdapter.getInvoicePDF( mockUserId, mockOrderId, mockInvoiceId, mockExternalSystemId ) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockFile); - done(); - }); + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -237,28 +237,27 @@ describe('OccPDFInvoicesAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); mockReq.flush(mockFile); expect(mockReq.request.responseType).toEqual('blob'); + + const result = await resultPromise; + expect(result).toEqual(mockFile); }); - it(`should result in error when Invoice download Error is thrown`, (done) => { - spyOn(httpClient, 'get').and.returnValue( + it(`should result in error when Invoice download Error is thrown`, async () => { + vi.spyOn(httpClient, 'get').mockReturnValue( throwError(mockDownloadPDFBadRequestResponse) ); let result: HttpErrorModel | undefined; - const subscription = occPDFInvoicesAdapter - .getInvoicePDF( + await firstValueFrom( + occPDFInvoicesAdapter.getInvoicePDF( mockUserId, mockOrderId, mockInvoiceId, mockExternalSystemId ) - .pipe(take(1)) - .subscribe({ - error: (err: any) => { - result = err; - done(); - }, - }); + ).catch((err: any) => { + result = err; + }); expect(result).toEqual( tryNormalizeHttpError( @@ -266,8 +265,6 @@ describe('OccPDFInvoicesAdapter', () => { new MockLoggerService() ) ); - - subscription.unsubscribe(); }); }); }); diff --git a/feature-libs/pdf-invoices/project.json b/feature-libs/pdf-invoices/project.json index cacffae258d..8fbef6ece63 100644 --- a/feature-libs/pdf-invoices/project.json +++ b/feature-libs/pdf-invoices/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/pdf-invoices/test.ts", - "tsConfig": "feature-libs/pdf-invoices/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/pdf-invoices/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/pdf-invoices/test.ts b/feature-libs/pdf-invoices/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/pdf-invoices/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/pdf-invoices/tsconfig.spec.json b/feature-libs/pdf-invoices/tsconfig.spec.json index c18562e56f7..557f701506a 100644 --- a/feature-libs/pdf-invoices/tsconfig.spec.json +++ b/feature-libs/pdf-invoices/tsconfig.spec.json @@ -2,14 +2,21 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "strict": false, "module": "preserve", - "types": ["jasmine", "node"], + "strict": false, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], "skipLibCheck": true, "resolveJsonModule": true, "esModuleInterop": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/pdf-invoices/vitest.config.ts b/feature-libs/pdf-invoices/vitest.config.ts new file mode 100644 index 00000000000..5ed4ef6e76b --- /dev/null +++ b/feature-libs/pdf-invoices/vitest.config.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + }, + }, + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/pdf-invoices`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-pdf-invoices.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts index 108b1b3469c..c5c22472795 100644 --- a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts @@ -1,11 +1,6 @@ import { CommonModule } from '@angular/common'; import { ElementRef } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActiveCartFacade, Cart, OrderEntry } from '@spartacus/cart/base/root'; import { CmsService, @@ -28,8 +23,9 @@ import { LaunchDialogService, OutletContextData, } from '@spartacus/storefront'; +import { vi } from 'vitest'; import { cold } from 'jasmine-marbles'; -import { Observable, of } from 'rxjs'; +import { Observable, firstValueFrom, of } from 'rxjs'; import { MockPickupLocationsSearchService } from '../../../core/facade/pickup-locations-search.service.spec'; import { MockPickupOptionFacade } from '../../../core/facade/pickup-option.service.spec'; import { MockPreferredStoreService } from '../../../core/services/preferred-store.service.spec'; @@ -188,7 +184,7 @@ describe('CartPickupOptionsContainerComponent', () => { launchDialogService = TestBed.inject(LaunchDialogService); activeCartService = TestBed.inject(ActiveCartFacade); pickupOptionService = TestBed.inject(PickupOptionFacade); - spyOn(launchDialogService, 'openDialog').and.callThrough(); + vi.spyOn(launchDialogService, 'openDialog'); fixture.detectChanges(); }; @@ -221,7 +217,7 @@ describe('CartPickupOptionsContainerComponent', () => { }); it('should not openDialog if display name is not set and ship it is selected', () => { - spyOn(component, 'openDialog'); + vi.spyOn(component, 'openDialog'); component['displayNameIsSet'] = false; const pickupOption: PickupOption = 'delivery'; const event = { @@ -232,7 +228,8 @@ describe('CartPickupOptionsContainerComponent', () => { expect(component.openDialog).not.toHaveBeenCalled(); }); - it('should check call update Entry on pickup option change when option is pickup', fakeAsync(() => { + it('should check call update Entry on pickup option change when option is pickup', async () => { + vi.useFakeTimers(); const entryNumber = 2; const pickupOption: PickupOption = 'pickup'; const quantity = 3; @@ -245,8 +242,8 @@ describe('CartPickupOptionsContainerComponent', () => { component.quantity = quantity; component['displayNameIsSet'] = false; - spyOn(pickupOptionService, 'setPickupOption'); - spyOn(activeCartService, 'updateEntry'); + vi.spyOn(pickupOptionService, 'setPickupOption'); + vi.spyOn(activeCartService, 'updateEntry'); component.onPickupOptionChange(event); expect(pickupOptionService.setPickupOption).toHaveBeenCalledWith( @@ -254,7 +251,8 @@ describe('CartPickupOptionsContainerComponent', () => { pickupOption ); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(activeCartService.updateEntry).toHaveBeenCalledWith( entryNumber, @@ -262,16 +260,16 @@ describe('CartPickupOptionsContainerComponent', () => { 'London School', true ); - })); + }); it('should set cartId to active cart id', () => { - spyOn(activeCartService, 'getActive').and.callThrough(); + vi.spyOn(activeCartService, 'getActive'); component.ngOnInit(); expect(component['cartId']).toBe('test-active-cart-code'); }); it('should call getPreferredStoreWithProductInStock', () => { - spyOn(activeCartService, 'getActive').and.callThrough(); + vi.spyOn(activeCartService, 'getActive'); component.ngOnInit(); expect(component['cartId']).toBe('test-active-cart-code'); }); @@ -306,7 +304,7 @@ describe('CartPickupOptionsContainerComponent', () => { }); it('should set the pickupOption to delivery', () => { - spyOn(pickupOptionService, 'getPickupOption').and.returnValue( + vi.spyOn(pickupOptionService, 'getPickupOption').mockReturnValue( of('delivery') ); expect(component.pickupOption$).toBeObservable( @@ -373,16 +371,16 @@ describe('CartPickupOptionsContainerComponent', () => { stubServiceAndCreateComponent(); }); - it('should set value for disableControls', (done) => { + it('should set value for disableControls', async () => { const mockEntries = [ { product: { code: 'ABC' } }, { product: { code: 'DEF' } }, ]; - spyOn(activeCartService, 'getEntries').and.returnValue(of(mockEntries)); - component.disableControls$.subscribe((result) => { - expect(result).toBe(false); - done(); - }); + vi.spyOn(activeCartService, 'getEntries').mockReturnValue( + of(mockEntries) + ); + const result = await firstValueFrom(component.disableControls$); + expect(result).toBe(false); }); }); }); diff --git a/feature-libs/pickup-in-store/components/container/my-preferred-store/my-preferred-store.component.spec.ts b/feature-libs/pickup-in-store/components/container/my-preferred-store/my-preferred-store.component.spec.ts index c09fe73e1f7..9330ee9f582 100644 --- a/feature-libs/pickup-in-store/components/container/my-preferred-store/my-preferred-store.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/my-preferred-store/my-preferred-store.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -233,17 +234,17 @@ describe('MyPreferredStoreComponent', () => { }); it('should changeStore', () => { - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); component.changeStore(); expect(routingService.go).toHaveBeenCalledWith(['/store-finder']); }); it('should show the link', () => { - spyOn(component, 'getDirectionsToStore'); - spyOn( + vi.spyOn(component, 'getDirectionsToStore'); + vi.spyOn( pickupLocationsSearchService, 'loadAndGetStoreDetails' - ).and.returnValue(of(mockStore)); + ).mockReturnValue(of(mockStore)); component.ngOnInit(); fixture.detectChanges(); @@ -256,13 +257,13 @@ describe('MyPreferredStoreComponent', () => { }); it('should show action link and a button', () => { - spyOn(cmsService, 'getCurrentPage').and.returnValue( + vi.spyOn(cmsService, 'getCurrentPage').mockReturnValue( of({ pageId: 'someOtherPage' }) ); - spyOn( + vi.spyOn( pickupLocationsSearchService, 'loadAndGetStoreDetails' - ).and.returnValue(of(mockStore)); + ).mockReturnValue(of(mockStore)); component.ngOnInit(); fixture.detectChanges(); diff --git a/feature-libs/pickup-in-store/components/container/pdp-pickup-options-container/pdp-pickup-options-container.component.spec.ts b/feature-libs/pickup-in-store/components/container/pdp-pickup-options-container/pdp-pickup-options-container.component.spec.ts index eaf8f651b06..27f25213fc4 100644 --- a/feature-libs/pickup-in-store/components/container/pdp-pickup-options-container/pdp-pickup-options-container.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/pdp-pickup-options-container/pdp-pickup-options-container.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -31,15 +32,13 @@ import { PickupOptionsStubComponent } from '../../presentational/pickup-options/ import { CurrentLocationService } from '../../services/current-location.service'; import { MockLaunchDialogService } from '../pickup-option-dialog/pickup-option-dialog.component.spec'; -import createSpy = jasmine.createSpy; - class MockPickupLocationsSearchFacade implements Partial { - startSearch = createSpy(); - hasSearchStarted = createSpy(); - isSearchRunning = createSpy(); - getSearchResults = createSpy().and.returnValue( + startSearch = vi.fn(); + hasSearchStarted = vi.fn(); + isSearchRunning = vi.fn(); + getSearchResults = vi.fn().mockReturnValue( of([ { name: 'preferredStore', @@ -49,16 +48,16 @@ class MockPickupLocationsSearchFacade }, ]) ); - clearSearchResults = createSpy(); - getHideOutOfStock = createSpy(); - setBrowserLocation = createSpy(); - toggleHideOutOfStock = createSpy(); - stockLevelAtStore = createSpy(); - getStockLevelAtStore = createSpy().and.returnValue( - of({ stockLevel: { displayName: 'London School' } }) - ); - getStoreDetails = createSpy().and.returnValue(of({ name: 'London School' })); - loadStoreDetails = createSpy(); + clearSearchResults = vi.fn(); + getHideOutOfStock = vi.fn(); + setBrowserLocation = vi.fn(); + toggleHideOutOfStock = vi.fn(); + stockLevelAtStore = vi.fn(); + getStockLevelAtStore = vi + .fn() + .mockReturnValue(of({ stockLevel: { displayName: 'London School' } })); + getStoreDetails = vi.fn().mockReturnValue(of({ name: 'London School' })); + loadStoreDetails = vi.fn(); } export class MockCurrentProductService { @@ -145,16 +144,10 @@ describe('PdpPickupOptionsComponent', () => { currentProductService = TestBed.inject(CurrentProductService); - spyOn(currentProductService, 'getProduct').and.callThrough(); - spyOn(launchDialogService, 'openDialog').and.callThrough(); - spyOn( - intendedPickupLocationService, - 'removeIntendedLocation' - ).and.callThrough(); - spyOn( - intendedPickupLocationService, - 'setIntendedLocation' - ).and.callThrough(); + vi.spyOn(currentProductService, 'getProduct'); + vi.spyOn(launchDialogService, 'openDialog'); + vi.spyOn(intendedPickupLocationService, 'removeIntendedLocation'); + vi.spyOn(intendedPickupLocationService, 'setIntendedLocation'); fixture.detectChanges(); }; @@ -170,7 +163,7 @@ describe('PdpPickupOptionsComponent', () => { }); it('should not open dialog', () => { - spyOn(component, 'openDialog'); + vi.spyOn(component, 'openDialog'); component.onPickupOptionChange({ option: 'pickup', triggerElement: {} as ElementRef, @@ -179,10 +172,10 @@ describe('PdpPickupOptionsComponent', () => { }); it('should handle invalid intended location on init', async () => { - spyOn( + vi.spyOn( intendedPickupLocationService, 'getIntendedLocation' - ).and.returnValue(of({ pickupOption: 'pickup', displayName: undefined })); + ).mockReturnValue(of({ pickupOption: 'pickup', displayName: undefined })); const displayLocation = await firstValueFrom( component.displayPickupLocation$ ); @@ -191,16 +184,13 @@ describe('PdpPickupOptionsComponent', () => { it('should unsubscribe from any subscriptions when destroyed', () => { component.subscription = new Subscription(); - spyOn(component.subscription, 'unsubscribe'); + vi.spyOn(component.subscription, 'unsubscribe'); component.ngOnDestroy(); expect(component.subscription.unsubscribe).toHaveBeenCalled(); }); it('should get the intended pickup location for the product on init', () => { - spyOn( - intendedPickupLocationService, - 'getIntendedLocation' - ).and.callThrough(); + vi.spyOn(intendedPickupLocationService, 'getIntendedLocation'); component.ngOnInit(); @@ -211,11 +201,11 @@ describe('PdpPickupOptionsComponent', () => { }); it('should return undefined if intendedLocation.displayName is not defined', async () => { - spyOn( + vi.spyOn( intendedPickupLocationService, 'getIntendedLocation' - ).and.returnValue(of({ pickupOption: 'pickup', displayName: undefined })); - spyOn(component, 'setIntendedPickupLocation'); + ).mockReturnValue(of({ pickupOption: 'pickup', displayName: undefined })); + vi.spyOn(component, 'setIntendedPickupLocation'); const displayLocation = await firstValueFrom( component.displayPickupLocation$ ); @@ -223,10 +213,10 @@ describe('PdpPickupOptionsComponent', () => { }); it('setIntendedPickupLocation should set pickupOption as delivery', async () => { - spyOn( + vi.spyOn( preferredStoreFacade, 'getPreferredStoreWithProductInStock' - ).and.returnValue( + ).mockReturnValue( of({ name: 'London School', displayName: 'London School' }) ); component.setIntendedPickupLocation('productCode'); @@ -249,10 +239,7 @@ describe('PdpPickupOptionsComponent', () => { }); it('should make no calls', () => { - spyOn( - intendedPickupLocationService, - 'getIntendedLocation' - ).and.callThrough(); + vi.spyOn(intendedPickupLocationService, 'getIntendedLocation'); component.ngOnInit(); @@ -303,7 +290,7 @@ describe('PdpPickupOptionsComponent', () => { }); it('should not call getPreferredStore if display name is set', () => { - spyOn(preferredStoreFacade, 'getPreferredStore$'); + vi.spyOn(preferredStoreFacade, 'getPreferredStore$'); expect(preferredStoreFacade.getPreferredStore$).not.toHaveBeenCalled(); }); diff --git a/feature-libs/pickup-in-store/components/container/pickup-info-container/pickup-info-container.component.spec.ts b/feature-libs/pickup-in-store/components/container/pickup-info-container/pickup-info-container.component.spec.ts index b66cda91cdc..68e15b205c2 100644 --- a/feature-libs/pickup-in-store/components/container/pickup-info-container/pickup-info-container.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/pickup-info-container/pickup-info-container.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActiveCartFacade, Cart, OrderEntry } from '@spartacus/cart/base/root'; import { @@ -79,9 +80,9 @@ describe('PickupInfoContainerComponent', () => { const result: Partial[] = [ { address: undefined, displayName: undefined, openingHours: undefined }, ]; - spyOn(activeCartService, 'getActive').and.callThrough(); - spyOn(pickupLocationsSearchService, 'loadStoreDetails').and.callThrough(); - spyOn(pickupLocationsSearchService, 'getStoreDetails').and.callThrough(); + vi.spyOn(activeCartService, 'getActive'); + vi.spyOn(pickupLocationsSearchService, 'loadStoreDetails'); + vi.spyOn(pickupLocationsSearchService, 'getStoreDetails'); component.ngOnInit(); expect(activeCartService.getActive).toHaveBeenCalled(); expect(pickupLocationsSearchService.loadStoreDetails).toHaveBeenCalledWith( diff --git a/feature-libs/pickup-in-store/components/container/pickup-option-dialog/pickup-option-dialog.component.spec.ts b/feature-libs/pickup-in-store/components/container/pickup-option-dialog/pickup-option-dialog.component.spec.ts index 89566f16686..a4111ac6e3f 100644 --- a/feature-libs/pickup-in-store/components/container/pickup-option-dialog/pickup-option-dialog.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/pickup-option-dialog/pickup-option-dialog.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { provideHttpClient, @@ -172,8 +173,8 @@ describe('PickupOptionDialogComponent', () => { }); it('ngOnInit should call appropriate methods', () => { - spyOn(pickupLocationsSearchService, 'getHideOutOfStock'); - spyOn(pickupOptionFacade, 'getPageContext').and.returnValue(of('PDP')); + vi.spyOn(pickupLocationsSearchService, 'getHideOutOfStock'); + vi.spyOn(pickupOptionFacade, 'getPageContext').mockReturnValue(of('PDP')); component.ngOnInit(); expect(component.isPDP).toEqual(true); @@ -181,7 +182,7 @@ describe('PickupOptionDialogComponent', () => { }); it('ngOnInit should set the cartId and userId for an anonymous user', () => { - spyOn(activeCartFacade, 'getActive').and.returnValue( + vi.spyOn(activeCartFacade, 'getActive').mockReturnValue( of({ guid: 'test', user: { uid: 'anonymous' }, @@ -194,7 +195,7 @@ describe('PickupOptionDialogComponent', () => { }); it('ngOnInit should set the cartId and userId for a logged in user', () => { - spyOn(activeCartFacade, 'getActive').and.returnValue( + vi.spyOn(activeCartFacade, 'getActive').mockReturnValue( of({ guid: 'test', user: { uid: 'test@sap.com' }, @@ -207,7 +208,7 @@ describe('PickupOptionDialogComponent', () => { }); it('onFindStores calls appropriate service method', () => { - spyOn(pickupLocationsSearchService, 'startSearch'); + vi.spyOn(pickupLocationsSearchService, 'startSearch'); component.onFindStores({ location: '' }); expect(pickupLocationsSearchService.startSearch).toHaveBeenCalledWith({ productCode: 'testProductCode', @@ -216,7 +217,7 @@ describe('PickupOptionDialogComponent', () => { }); it('onHideOutOfStock calls appropriate service method', () => { - spyOn(pickupLocationsSearchService, 'toggleHideOutOfStock'); + vi.spyOn(pickupLocationsSearchService, 'toggleHideOutOfStock'); component.onHideOutOfStock(); expect( pickupLocationsSearchService.toggleHideOutOfStock @@ -225,7 +226,7 @@ describe('PickupOptionDialogComponent', () => { it('should close dialog on close method', () => { const mockCloseReason = 'Close Dialog'; - spyOn(launchDialogService, 'closeDialog'); + vi.spyOn(launchDialogService, 'closeDialog'); component.close(mockCloseReason); expect(launchDialogService.closeDialog).toHaveBeenCalledWith( @@ -236,12 +237,9 @@ describe('PickupOptionDialogComponent', () => { it('should close dialog on close method no selection', () => { const mockCloseReason = 'CLOSE_WITHOUT_SELECTION'; component.productCode = 'productCode'; - spyOn(launchDialogService, 'closeDialog'); - spyOn( - intendedPickupLocationFacade, - 'getIntendedLocation' - ).and.callThrough(); - spyOn(intendedPickupLocationFacade, 'setPickupOption').and.callThrough(); + vi.spyOn(launchDialogService, 'closeDialog'); + vi.spyOn(intendedPickupLocationFacade, 'getIntendedLocation'); + vi.spyOn(intendedPickupLocationFacade, 'setPickupOption'); component.close(mockCloseReason); @@ -262,10 +260,11 @@ describe('PickupOptionDialogComponent', () => { it('should filter if store name is defined', () => { const mockCloseReason = 'CLOSE_WITHOUT_SELECTION'; - spyOn(intendedPickupLocationFacade, 'getIntendedLocation').and.returnValue( - of({ name: 'testStoreName', pickupOption: 'pickup' }) - ); - spyOn(launchDialogService, 'closeDialog'); + vi.spyOn( + intendedPickupLocationFacade, + 'getIntendedLocation' + ).mockReturnValue(of({ name: 'testStoreName', pickupOption: 'pickup' })); + vi.spyOn(launchDialogService, 'closeDialog'); component.close(mockCloseReason); expect(launchDialogService.closeDialog).toHaveBeenCalledWith( @@ -275,7 +274,7 @@ describe('PickupOptionDialogComponent', () => { it('should close the dialog when user clicks outside', () => { const element = fixture.debugElement.nativeElement; - spyOn(component, 'close'); + vi.spyOn(component, 'close'); element.click(); expect(component.close).toHaveBeenCalledWith( @@ -287,7 +286,7 @@ describe('PickupOptionDialogComponent', () => { const element = ( fixture.debugElement.nativeElement as HTMLElement ).querySelector('.cx-pickup-option-dialog'); - spyOn(component, 'close'); + vi.spyOn(component, 'close'); element?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); expect(component.close).toHaveBeenCalledWith( diff --git a/feature-libs/pickup-in-store/components/container/set-preferred-store/set-preferred-store.component.spec.ts b/feature-libs/pickup-in-store/components/container/set-preferred-store/set-preferred-store.component.spec.ts index f104a5e0f05..112b201ee3e 100644 --- a/feature-libs/pickup-in-store/components/container/set-preferred-store/set-preferred-store.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/set-preferred-store/set-preferred-store.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -50,7 +51,7 @@ describe('SetPreferredStoreComponent without outlet.context$', () => { }); it('should call setPreferredStore on preferredStoreFacade with pointOfServiceName', () => { - spyOn(preferredStoreFacade, 'setPreferredStore'); + vi.spyOn(preferredStoreFacade, 'setPreferredStore'); component.setAsPreferred(); expect(preferredStoreFacade.setPreferredStore).toHaveBeenCalledWith( @@ -104,7 +105,7 @@ describe('SetPreferredStoreComponent with outlet.context$', () => { }); it('should call setPreferredStore on preferredStoreFacade with pointOfServiceName', () => { - spyOn(preferredStoreFacade, 'setPreferredStore'); + vi.spyOn(preferredStoreFacade, 'setPreferredStore'); component.setAsPreferred(); expect(preferredStoreFacade.setPreferredStore).toHaveBeenCalledWith( diff --git a/feature-libs/pickup-in-store/components/container/store-list/store-list.component.spec.ts b/feature-libs/pickup-in-store/components/container/store-list/store-list.component.spec.ts index 6f111004fc4..6c6e786ec63 100644 --- a/feature-libs/pickup-in-store/components/container/store-list/store-list.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/store-list/store-list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { provideHttpClient, withInterceptorsFromDi, @@ -63,9 +64,9 @@ describe('StoreListComponent', () => { }); it('should get local stores on init', () => { - spyOn(pickupLocationsSearchService, 'getSearchResults'); - spyOn(pickupLocationsSearchService, 'isSearchRunning'); - spyOn(pickupLocationsSearchService, 'hasSearchStarted'); + vi.spyOn(pickupLocationsSearchService, 'getSearchResults'); + vi.spyOn(pickupLocationsSearchService, 'isSearchRunning'); + vi.spyOn(pickupLocationsSearchService, 'hasSearchStarted'); component.ngOnInit(); expect(pickupLocationsSearchService.getSearchResults).toHaveBeenCalledWith( @@ -77,7 +78,7 @@ describe('StoreListComponent', () => { it('should call getSearchResults with productCode', () => { component.productCode = 'productCode'; - spyOn(pickupLocationsSearchService, 'getSearchResults'); + vi.spyOn(pickupLocationsSearchService, 'getSearchResults'); component.ngOnInit(); expect(pickupLocationsSearchService.getSearchResults).toHaveBeenCalledWith( 'productCode' @@ -85,7 +86,7 @@ describe('StoreListComponent', () => { }); it('should emit storeSelected', () => { - spyOn(component.storeSelected, 'emit'); + vi.spyOn(component.storeSelected, 'emit'); const pointOfService = { name: 'Store Name', displayName: 'Store Name', @@ -95,7 +96,7 @@ describe('StoreListComponent', () => { }); it('should call setIntendedLocation on IntendedPickupLocationService', () => { - spyOn(intendedPickupLocationService, 'setIntendedLocation'); + vi.spyOn(intendedPickupLocationService, 'setIntendedLocation'); const store: PointOfServiceStock = { stockInfo: {} }; const location: AugmentedPointOfService = { pickupOption: 'pickup' }; component.onSelectStore(store); diff --git a/feature-libs/pickup-in-store/components/container/store-search/store-search.component.spec.ts b/feature-libs/pickup-in-store/components/container/store-search/store-search.component.spec.ts index a8778e7329c..5531e523a6a 100644 --- a/feature-libs/pickup-in-store/components/container/store-search/store-search.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/store-search/store-search.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; @@ -34,17 +35,18 @@ describe('StoreSearchComponent', () => { fixture = TestBed.createComponent(StoreSearchComponent); currentLocationService = TestBed.inject(CurrentLocationService); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('onFindStores emits a location and returns false', () => { + fixture.detectChanges(); const location = 'a location'; - spyOn(component, 'onFindStores').and.callThrough(); - spyOn(component.findStores, 'emit').and.callThrough(); + vi.spyOn(component, 'onFindStores'); + vi.spyOn(component.findStores, 'emit'); const RESULT = component.onFindStores(location); expect(component.onFindStores).toHaveBeenCalledWith(location); expect(component.findStores.emit).toHaveBeenCalledWith({ location }); @@ -52,20 +54,21 @@ describe('StoreSearchComponent', () => { }); it('onHideOutOfStock emits eventHideOutOfStock', () => { - spyOn(component.eventHideOutOfStock, 'emit'); + fixture.detectChanges(); + vi.spyOn(component.eventHideOutOfStock, 'emit'); expect(component.hideOutOfStock).toEqual(false); component.onHideOutOfStock(); expect(component.eventHideOutOfStock.emit).toHaveBeenCalledWith(true); - component.hideOutOfStock = !component.hideOutOfStock; - fixture.detectChanges(); + fixture.componentRef.setInput('hideOutOfStock', true); component.onHideOutOfStock(); expect(component.eventHideOutOfStock.emit).toHaveBeenCalledWith(false); }); it('useMyLocation makes findStores emit a location', () => { - spyOn(currentLocationService, 'getCurrentLocation').and.callThrough(); - spyOn(component.showSpinner, 'emit').and.callThrough(); - spyOn(component.findStores, 'emit').and.callThrough(); + fixture.detectChanges(); + vi.spyOn(currentLocationService, 'getCurrentLocation'); + vi.spyOn(component.showSpinner, 'emit'); + vi.spyOn(component.findStores, 'emit'); component.useMyLocation(); diff --git a/feature-libs/pickup-in-store/components/presentational/pickup-options/pickup-options.component.spec.ts b/feature-libs/pickup-in-store/components/presentational/pickup-options/pickup-options.component.spec.ts index e18227b1e48..c1485988f07 100644 --- a/feature-libs/pickup-in-store/components/presentational/pickup-options/pickup-options.component.spec.ts +++ b/feature-libs/pickup-in-store/components/presentational/pickup-options/pickup-options.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -52,7 +53,7 @@ describe('PickupOptionsComponent', () => { )[PickupOptionsTabs.DELIVERY].nativeElement; expect(activeTab.classList.contains('active')).toBeTruthy(); - spyOn(component.tabComponent, 'select').and.callThrough(); + vi.spyOn(component.tabComponent, 'select'); component.selectedOption = 'pickup'; component.ngOnChanges(); fixture.detectChanges(); @@ -63,7 +64,7 @@ describe('PickupOptionsComponent', () => { }); it('should emit the new pickup option on onPickupOptionChange', () => { - spyOn(component.pickupOptionChange, 'emit'); + vi.spyOn(component.pickupOptionChange, 'emit'); component.onPickupOptionChange('delivery'); expect(component.pickupOptionChange.emit).toHaveBeenCalledWith({ @@ -73,7 +74,7 @@ describe('PickupOptionsComponent', () => { }); it('should emit on onPickupLocationChange', () => { - spyOn(component.pickupLocationChange, 'emit'); + vi.spyOn(component.pickupLocationChange, 'emit'); component.onPickupLocationChange(); expect(component.pickupLocationChange.emit).toHaveBeenCalled(); @@ -123,7 +124,7 @@ describe('PickupOptionsComponent', () => { }); it('should call onPickupOptionChange when the tab is changed', () => { - spyOn(component, 'onPickupOptionChange'); + vi.spyOn(component, 'onPickupOptionChange'); fixture.detectChanges(); // for delivery @@ -144,7 +145,7 @@ describe('PickupOptionsComponent', () => { }); it('should call onPickupLocationChange when the select store button is clicked', () => { - spyOn(component, 'onPickupLocationChange'); + vi.spyOn(component, 'onPickupLocationChange'); fixture.detectChanges(); const selectStoreButton = fixture.debugElement.query( @@ -157,7 +158,7 @@ describe('PickupOptionsComponent', () => { it('should call onPickupLocationChange when the change store button is clicked', () => { fixture.detectChanges(); - spyOn(component, 'onPickupLocationChange'); + vi.spyOn(component, 'onPickupLocationChange'); component.selectedOption = 'pickup'; component.displayPickupLocation = 'Test location'; component.ngOnChanges(); diff --git a/feature-libs/pickup-in-store/components/presentational/store/store.component.spec.ts b/feature-libs/pickup-in-store/components/presentational/store/store.component.spec.ts index e7978fe150f..09e112b0fd8 100644 --- a/feature-libs/pickup-in-store/components/presentational/store/store.component.spec.ts +++ b/feature-libs/pickup-in-store/components/presentational/store/store.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; @@ -48,15 +49,15 @@ describe('StoreComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(StoreComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('selectStore emits the storeDetails and returns false', () => { - spyOn(component.storeSelected, 'emit'); + vi.spyOn(component.storeSelected, 'emit'); component.storeDetails = { name: 'storeName' }; fixture.detectChanges(); @@ -98,6 +99,7 @@ describe('StoreComponent', () => { }); it('toggleOpenHours toggles the value of openHoursOpen', () => { + fixture.detectChanges(); const element = fixture.debugElement.nativeElement; expect(component.openHoursOpen).toEqual(false); @@ -109,7 +111,11 @@ describe('StoreComponent', () => { ICON_TYPE.CARET_DOWN ); - component.toggleOpenHours(); + const toggleButton = fixture.debugElement.query( + By.css('.cx-store-opening-hours-toggle') + ).nativeElement; + + toggleButton.click(); fixture.detectChanges(); expect(component.openHoursOpen).toEqual(true); expect(element.querySelector('cx-store-schedule')).not.toBeNull(); @@ -118,7 +124,7 @@ describe('StoreComponent', () => { ); expect(iconDebugElement.componentInstance.type).toEqual(ICON_TYPE.CARET_UP); - component.toggleOpenHours(); + toggleButton.click(); fixture.detectChanges(); expect(component.openHoursOpen).toEqual(false); expect(element.querySelector('cx-store-schedule')).toBeNull(); diff --git a/feature-libs/pickup-in-store/components/services/current-location.service.spec.ts b/feature-libs/pickup-in-store/components/services/current-location.service.spec.ts index 0db0dc60b36..cc374b98da9 100644 --- a/feature-libs/pickup-in-store/components/services/current-location.service.spec.ts +++ b/feature-libs/pickup-in-store/components/services/current-location.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { WindowRef } from '@spartacus/core'; import { CurrentLocationService } from './current-location.service'; @@ -57,12 +58,12 @@ describe('CurrentLocationService', () => { }); it('should get the current location from the browser API', () => { - spyOn( + vi.spyOn( (windowRef.nativeWindow as Window).navigator.geolocation, 'getCurrentPosition' - ).and.callThrough(); + ); - const successCallback: PositionCallback = jasmine.createSpy(); + const successCallback: PositionCallback = vi.fn(); const errorCallback: PositionErrorCallback = () => {}; const options: PositionOptions = {}; @@ -83,7 +84,7 @@ describe('CurrentLocationService', () => { }); it('should do nothing if the native window is undefined', () => { - const successCallback: PositionCallback = jasmine.createSpy(); + const successCallback: PositionCallback = vi.fn(); const errorCallback: PositionErrorCallback = () => {}; const options: PositionOptions = {}; diff --git a/feature-libs/pickup-in-store/components/services/delivery-points.service.spec.ts b/feature-libs/pickup-in-store/components/services/delivery-points.service.spec.ts index 46139fda453..6287acd0f7e 100644 --- a/feature-libs/pickup-in-store/components/services/delivery-points.service.spec.ts +++ b/feature-libs/pickup-in-store/components/services/delivery-points.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; import { PointOfService } from '@spartacus/core'; @@ -93,10 +94,10 @@ describe('DeliveryPointsService', () => { pickupLocationsSearchService = TestBed.inject(PickupLocationsSearchFacade); orderFacade = TestBed.inject(OrderFacade); - spyOn(activeCartFacade, 'getPickupEntries').and.callThrough(); - spyOn(pickupLocationsSearchService, 'loadStoreDetails').and.callThrough(); - spyOn(pickupLocationsSearchService, 'getStoreDetails').and.callThrough(); - spyOn(orderFacade, 'getPickupEntries').and.callThrough(); + vi.spyOn(activeCartFacade, 'getPickupEntries'); + vi.spyOn(pickupLocationsSearchService, 'loadStoreDetails'); + vi.spyOn(pickupLocationsSearchService, 'getStoreDetails'); + vi.spyOn(orderFacade, 'getPickupEntries'); }); it('should be created', () => { diff --git a/feature-libs/pickup-in-store/core/connectors/pickup-location.connector.spec.ts b/feature-libs/pickup-in-store/core/connectors/pickup-location.connector.spec.ts index 1135db1f90b..e8d0a874f30 100644 --- a/feature-libs/pickup-in-store/core/connectors/pickup-location.connector.spec.ts +++ b/feature-libs/pickup-in-store/core/connectors/pickup-location.connector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { PointOfService } from '@spartacus/core'; @@ -5,7 +6,6 @@ import { Observable, of } from 'rxjs'; import { PickupLocationAdapter } from './pickup-location.adapter'; import { PickupLocationConnector } from './pickup-location.connector'; -import createSpy = jasmine.createSpy; export class MockPickupLocationConnector { getStoreDetails(_storeName: string): Observable { @@ -21,7 +21,7 @@ export class MockPickupLocationConnectorWithError { } class MockPickupLocationAdapter implements PickupLocationAdapter { - getStoreDetails = createSpy(); + getStoreDetails = vi.fn(); } describe('PickupLocationConnector', () => { diff --git a/feature-libs/pickup-in-store/core/connectors/stock.connector.spec.ts b/feature-libs/pickup-in-store/core/connectors/stock.connector.spec.ts index 89903052be7..e20cd2c1549 100644 --- a/feature-libs/pickup-in-store/core/connectors/stock.connector.spec.ts +++ b/feature-libs/pickup-in-store/core/connectors/stock.connector.spec.ts @@ -1,16 +1,16 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { StockAdapter } from './stock.adapter'; import { StockConnector } from './stock.connector'; -import createSpy = jasmine.createSpy; describe('StockConnector', () => { let service: StockConnector; let adapter: StockAdapter; const MockStockAdapter = { - loadStockLevels: createSpy(), - loadStockLevelAtStore: createSpy(), + loadStockLevels: vi.fn(), + loadStockLevelAtStore: vi.fn(), }; beforeEach(() => { diff --git a/feature-libs/pickup-in-store/core/facade/intended-pickup-location.service.spec.ts b/feature-libs/pickup-in-store/core/facade/intended-pickup-location.service.spec.ts index 90d0f4218c2..2639671f1c3 100644 --- a/feature-libs/pickup-in-store/core/facade/intended-pickup-location.service.spec.ts +++ b/feature-libs/pickup-in-store/core/facade/intended-pickup-location.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { @@ -21,8 +22,8 @@ describe('IntendedPickupLocationService', () => { service = TestBed.inject(IntendedPickupLocationService); store = TestBed.inject(Store); - spyOn(store, 'dispatch'); - spyOn(store, 'pipe'); + vi.spyOn(store, 'dispatch'); + vi.spyOn(store, 'pipe'); }); it('should be created', () => { diff --git a/feature-libs/pickup-in-store/core/facade/pickup-locations-search.service.spec.ts b/feature-libs/pickup-in-store/core/facade/pickup-locations-search.service.spec.ts index 2beef3d896e..14696f639ff 100644 --- a/feature-libs/pickup-in-store/core/facade/pickup-locations-search.service.spec.ts +++ b/feature-libs/pickup-in-store/core/facade/pickup-locations-search.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { PointOfService, PointOfServiceStock, Stock } from '@spartacus/core'; @@ -91,7 +92,7 @@ describe('PickupLocationsSearchService', () => { service = TestBed.inject(PickupLocationsSearchService); store = TestBed.inject(MockStore); - spyOn(store, 'dispatch'); + vi.spyOn(store, 'dispatch'); }); it('should be created', () => { @@ -116,13 +117,13 @@ describe('PickupLocationsSearchService', () => { }); it('getStockLoading', () => { - spyOn(store, 'pipe'); + vi.spyOn(store, 'pipe'); service.isSearchRunning(); expect(store.pipe).toHaveBeenCalled(); }); it('getHideOutOfStockState', () => { - spyOn(store, 'pipe'); + vi.spyOn(store, 'pipe'); service.getHideOutOfStock(); expect(store.pipe).toHaveBeenCalled(); }); @@ -135,13 +136,13 @@ describe('PickupLocationsSearchService', () => { }); it('hasSearchBeenStartedForProductCode', () => { - spyOn(store, 'pipe'); + vi.spyOn(store, 'pipe'); service.hasSearchStarted('productCode'); expect(store.pipe).toHaveBeenCalled(); }); it('getStoresWithStockForProductCode', () => { - spyOn(store, 'pipe'); + vi.spyOn(store, 'pipe'); service.getSearchResults('productCode'); expect(store.pipe).toHaveBeenCalled(); }); @@ -159,7 +160,7 @@ describe('PickupLocationsSearchService', () => { }); it('getStoreDetails', () => { - spyOn(store, 'pipe'); + vi.spyOn(store, 'pipe'); service.getStoreDetails('name'); expect(store.pipe).toHaveBeenCalled(); }); @@ -183,7 +184,7 @@ describe('PickupLocationsSearchService', () => { }); it('getStockLevelAtStore', () => { - spyOn(store, 'pipe'); + vi.spyOn(store, 'pipe'); service.getStockLevelAtStore('productCode', 'name'); expect(store.pipe).toHaveBeenCalled(); }); diff --git a/feature-libs/pickup-in-store/core/facade/pickup-option.service.spec.ts b/feature-libs/pickup-in-store/core/facade/pickup-option.service.spec.ts index db437bc97fd..a8b186676af 100644 --- a/feature-libs/pickup-in-store/core/facade/pickup-option.service.spec.ts +++ b/feature-libs/pickup-in-store/core/facade/pickup-option.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { PickupOption } from '@spartacus/pickup-in-store/root'; @@ -32,8 +33,8 @@ describe('PickupOptionFacade', () => { service = TestBed.inject(PickupOptionService); store = TestBed.inject(Store); - spyOn(store, 'dispatch'); - spyOn(store, 'pipe'); + vi.spyOn(store, 'dispatch'); + vi.spyOn(store, 'pipe'); }); it('should be created', () => { diff --git a/feature-libs/pickup-in-store/core/services/preferred-store.service.spec.ts b/feature-libs/pickup-in-store/core/services/preferred-store.service.spec.ts index 7276adcfdb5..9b412cf1b3b 100644 --- a/feature-libs/pickup-in-store/core/services/preferred-store.service.spec.ts +++ b/feature-libs/pickup-in-store/core/services/preferred-store.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { WindowRef } from '@spartacus/core'; @@ -84,8 +85,8 @@ describe('PreferredStoreService', () => { pickupLocationSearchService = TestBed.inject(PickupLocationsSearchFacade); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(store, 'pipe').and.callThrough(); + vi.spyOn(store, 'dispatch'); + vi.spyOn(store, 'pipe'); }; describe('with pickup in store config', () => { @@ -128,14 +129,14 @@ describe('PreferredStoreService', () => { }; const productCode = 'P001'; - spyOn(preferredStoreFacade, 'getPreferredStore$').and.returnValue( + vi.spyOn(preferredStoreFacade, 'getPreferredStore$').mockReturnValue( of(preferredStore) ); - spyOn(pickupLocationSearchService, 'stockLevelAtStore').and.callThrough(); - spyOn( + vi.spyOn(pickupLocationSearchService, 'stockLevelAtStore'); + vi.spyOn( pickupLocationSearchService, 'getStockLevelAtStore' - ).and.returnValue(of({ stockLevelStatus: 'inStock' })); + ).mockReturnValue(of({ stockLevelStatus: 'inStock' })); const preferredStoreWithStock = preferredStoreFacade.getPreferredStoreWithProductInStock(productCode); diff --git a/feature-libs/pickup-in-store/core/store/effects/default-point-of-service-name.effect.spec.ts b/feature-libs/pickup-in-store/core/store/effects/default-point-of-service-name.effect.spec.ts index 8e5f6edba2e..ca12817fd6d 100644 --- a/feature-libs/pickup-in-store/core/store/effects/default-point-of-service-name.effect.spec.ts +++ b/feature-libs/pickup-in-store/core/store/effects/default-point-of-service-name.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, provideHttpClient, @@ -92,7 +93,7 @@ describe('DefaultPointOfServiceEffect', () => { }); it('should fetch preferred store value from localstorage if its not present in userProfile and call LoadDefaultPointOfServiceSuccess', () => { - spyOn(userProfileService, 'get').and.returnValue(of({})); + vi.spyOn(userProfileService, 'get').mockReturnValue(of({})); winRef.localStorage?.setItem( 'preferred_store', @@ -118,7 +119,7 @@ describe('DefaultPointOfServiceEffect', () => { it('should emit empty name and displayName if there is a error', () => { const error = new HttpErrorResponse({ error: 'error' }); - spyOn(userProfileService, 'get').and.returnValue( + vi.spyOn(userProfileService, 'get').mockReturnValue( new Observable((subscriber) => subscriber.error(error)) ); const action = LoadDefaultPointOfService(); diff --git a/feature-libs/pickup-in-store/core/store/effects/pickup-location.effect.spec.ts b/feature-libs/pickup-in-store/core/store/effects/pickup-location.effect.spec.ts index ca8bc5a693a..495a2726560 100644 --- a/feature-libs/pickup-in-store/core/store/effects/pickup-location.effect.spec.ts +++ b/feature-libs/pickup-in-store/core/store/effects/pickup-location.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, provideHttpClient, @@ -56,7 +57,7 @@ describe('PickupLocationEffect', () => { }); it('should call the connection on the GET_STORE_DETAILS action and create SetStoreDetailsSuccess action', () => { - spyOn(pickupLocationConnector, 'getStoreDetails').and.callThrough(); + vi.spyOn(pickupLocationConnector, 'getStoreDetails'); const action = GetStoreDetailsById({ payload: 'storeId' }); const actionSuccess = SetStoreDetailsSuccess({ payload: {} }); actions$ = hot('-a', { a: action }); @@ -90,7 +91,7 @@ describe('PickupLocationEffect with Error', () => { }); it('should call the connection on the GET_STORE_DETAILS action and create SetStoreDetailsFailure action', () => { - spyOn(pickupLocationConnector, 'getStoreDetails').and.callThrough(); + vi.spyOn(pickupLocationConnector, 'getStoreDetails'); const action = GetStoreDetailsById({ payload: 'storeId' }); const error = new HttpErrorResponse({ error: 'error' }); diff --git a/feature-libs/pickup-in-store/core/store/effects/stock.effect.spec.ts b/feature-libs/pickup-in-store/core/store/effects/stock.effect.spec.ts index 075f9057e9f..633ca69cba9 100644 --- a/feature-libs/pickup-in-store/core/store/effects/stock.effect.spec.ts +++ b/feature-libs/pickup-in-store/core/store/effects/stock.effect.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, provideHttpClient, @@ -59,7 +60,7 @@ describe('StockEffect', () => { }); it('should call the connector on the StockLevel action and create success action', () => { - spyOn(stockConnector, 'loadStockLevels').and.callThrough(); + vi.spyOn(stockConnector, 'loadStockLevels'); const action = new StockLevel({ productCode: 'P0001', location: '' }); const actionSuccess = new StockLevelSuccess({ productCode: 'P0001', @@ -81,7 +82,7 @@ describe('StockEffect', () => { statusText: 'Not Found', error: 'Error', }); - spyOn(stockConnector, 'loadStockLevels').and.returnValue( + vi.spyOn(stockConnector, 'loadStockLevels').mockReturnValue( throwError(() => error) ); const action = new StockLevel({ productCode: 'P0001', location: '' }); @@ -96,7 +97,7 @@ describe('StockEffect', () => { }); it('should call the connector on the StockLevelAtStore action and create StockLevelAtStoreSuccess action', () => { - spyOn(stockConnector, 'loadStockLevelAtStore').and.callThrough(); + vi.spyOn(stockConnector, 'loadStockLevelAtStore'); const action = StockLevelAtStore({ payload: { productCode: 'P0001', storeName: 'London School' }, }); diff --git a/feature-libs/pickup-in-store/core/store/reducers/stock/index.spec.ts b/feature-libs/pickup-in-store/core/store/reducers/stock/index.spec.ts index 51e40d1008b..6f0acc6fdcb 100644 --- a/feature-libs/pickup-in-store/core/store/reducers/stock/index.spec.ts +++ b/feature-libs/pickup-in-store/core/store/reducers/stock/index.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Action, ActionReducer } from '@ngrx/store'; import { ClearStockData, StockLevel } from '../../actions/stock.action'; import { StockState } from '../../stock-state'; @@ -19,8 +20,7 @@ describe('Stock meta-reducer', () => { it('should clear stock state for ClearStockData action', () => { const action = new ClearStockData(); - const reducer: ActionReducer = - jasmine.createSpy('reducer'); + const reducer: ActionReducer = vi.fn(); clearStockState(reducer)(state, action); expect(reducer).toHaveBeenCalledWith(undefined, action); @@ -29,8 +29,7 @@ describe('Stock meta-reducer', () => { it('should not clear stock state for other actions', () => { const action = new StockLevel({ productCode: 'code', location: '' }); - const reducer: ActionReducer = - jasmine.createSpy('reducer'); + const reducer: ActionReducer = vi.fn(); clearStockState(reducer)(state, action); expect(reducer).toHaveBeenCalledWith(state, action); diff --git a/feature-libs/pickup-in-store/karma.conf.js b/feature-libs/pickup-in-store/karma.conf.js deleted file mode 100644 index 55da99a9135..00000000000 --- a/feature-libs/pickup-in-store/karma.conf.js +++ /dev/null @@ -1,52 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-pickup-in-store.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/pickup-in-store'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 85, - functions: 90, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/pickup-in-store/occ/adapters/occ-pickup-location.adapter.spec.ts b/feature-libs/pickup-in-store/occ/adapters/occ-pickup-location.adapter.spec.ts index 8eaf48c1b9b..395c7dd7986 100644 --- a/feature-libs/pickup-in-store/occ/adapters/occ-pickup-location.adapter.spec.ts +++ b/feature-libs/pickup-in-store/occ/adapters/occ-pickup-location.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpClient, HttpErrorResponse, @@ -8,7 +9,7 @@ import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { BaseOccUrlProperties, DynamicAttributes, @@ -63,7 +64,7 @@ describe(`OccPickupLocationAdapter`, () => { let httpMock: HttpTestingController; let occEndpointService: OccEndpointsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ OccPickupLocationAdapter, @@ -73,14 +74,14 @@ describe(`OccPickupLocationAdapter`, () => { provideHttpClientTesting(), ], }); - })); + }); beforeEach(() => { occAdapter = TestBed.inject(OccPickupLocationAdapter); httpMock = TestBed.inject(HttpTestingController); httpClient = TestBed.inject(HttpClient); occEndpointService = TestBed.inject(OccEndpointsService); - spyOn(occEndpointService, 'buildUrl').and.callThrough(); + vi.spyOn(occEndpointService, 'buildUrl'); }); afterEach(() => { httpMock.verify(); @@ -105,18 +106,22 @@ describe(`OccPickupLocationAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); }); - it('should call normalized http error for getStoreDetails', fakeAsync(() => { - spyOn(httpClient, 'get').and.returnValue(throwError(() => mockJaloError)); + it('should call normalized http error for getStoreDetails', async () => { + vi.useFakeTimers(); + vi.spyOn(httpClient, 'get').mockReturnValue( + throwError(() => mockJaloError) + ); let result: HttpErrorModel | undefined; const subscription = occAdapter .getStoreDetails(storeName) .pipe(take(1)) .subscribe({ error: (err) => (result = err) }); - tick(4200); + await vi.advanceTimersByTimeAsync(4200); + vi.useRealTimers(); expect(result).toEqual(mockNormalizedJaloError); subscription.unsubscribe(); - })); + }); }); }); diff --git a/feature-libs/pickup-in-store/occ/adapters/occ-stock.adapter.spec.ts b/feature-libs/pickup-in-store/occ/adapters/occ-stock.adapter.spec.ts index 75246655a5e..80527c3b974 100644 --- a/feature-libs/pickup-in-store/occ/adapters/occ-stock.adapter.spec.ts +++ b/feature-libs/pickup-in-store/occ/adapters/occ-stock.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpClient, HttpErrorResponse, @@ -8,7 +9,7 @@ import { HttpTestingController, provideHttpClientTesting, } from '@angular/common/http/testing'; -import { TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { BaseOccUrlProperties, DynamicAttributes, @@ -64,7 +65,7 @@ describe(`OccStockAdapter`, () => { let httpMock: HttpTestingController; let occEndpointService: OccEndpointsService; let httpClient: HttpClient; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ OccStockAdapter, @@ -74,13 +75,13 @@ describe(`OccStockAdapter`, () => { provideHttpClientTesting(), ], }); - })); + }); beforeEach(() => { occAdapter = TestBed.inject(OccStockAdapter); httpMock = TestBed.inject(HttpTestingController); occEndpointService = TestBed.inject(OccEndpointsService); httpClient = TestBed.inject(HttpClient); - spyOn(occEndpointService, 'buildUrl').and.callThrough(); + vi.spyOn(occEndpointService, 'buildUrl'); }); afterEach(() => { httpMock.verify(); @@ -102,19 +103,23 @@ describe(`OccStockAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); }); - it('should call normalized http error for loadStockLevels', fakeAsync(() => { - spyOn(httpClient, 'get').and.returnValue(throwError(() => mockJaloError)); + it('should call normalized http error for loadStockLevels', async () => { + vi.useFakeTimers(); + vi.spyOn(httpClient, 'get').mockReturnValue( + throwError(() => mockJaloError) + ); let result: HttpErrorModel | undefined; const subscription = occAdapter .loadStockLevels(productCode, locationParam) .pipe(take(1)) .subscribe({ error: (err) => (result = err) }); - tick(4200); + await vi.advanceTimersByTimeAsync(4200); + vi.useRealTimers(); expect(result).toEqual(mockNormalizedJaloError); subscription.unsubscribe(); - })); + }); }); describe(`get loadStockLevelAtStore`, () => { it(`should get loadStockLevelAtStore`, () => { @@ -132,18 +137,22 @@ describe(`OccStockAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); }); - it('should call normalized http error for loadStockLevelAtStore', fakeAsync(() => { - spyOn(httpClient, 'get').and.returnValue(throwError(() => mockJaloError)); + it('should call normalized http error for loadStockLevelAtStore', async () => { + vi.useFakeTimers(); + vi.spyOn(httpClient, 'get').mockReturnValue( + throwError(() => mockJaloError) + ); let result: HttpErrorModel | undefined; const subscription = occAdapter .loadStockLevelAtStore(productCode, storeName) .pipe(take(1)) .subscribe({ error: (err) => (result = err) }); - tick(4200); + await vi.advanceTimersByTimeAsync(4200); + vi.useRealTimers(); expect(result).toEqual(mockNormalizedJaloError); subscription.unsubscribe(); - })); + }); }); }); diff --git a/feature-libs/pickup-in-store/project.json b/feature-libs/pickup-in-store/project.json index 9e9e71dff9a..43f5cb886d2 100644 --- a/feature-libs/pickup-in-store/project.json +++ b/feature-libs/pickup-in-store/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/pickup-in-store/test.ts", - "tsConfig": "feature-libs/pickup-in-store/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/pickup-in-store/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/pickup-in-store/test.ts b/feature-libs/pickup-in-store/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/pickup-in-store/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/pickup-in-store/tsconfig.spec.json b/feature-libs/pickup-in-store/tsconfig.spec.json index 3c36fd6d4e0..d52c68cbde6 100644 --- a/feature-libs/pickup-in-store/tsconfig.spec.json +++ b/feature-libs/pickup-in-store/tsconfig.spec.json @@ -2,11 +2,18 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "strict": false, "module": "preserve", - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "strict": false, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/pickup-in-store/vitest.config.ts b/feature-libs/pickup-in-store/vitest.config.ts new file mode 100644 index 00000000000..826283eb0d5 --- /dev/null +++ b/feature-libs/pickup-in-store/vitest.config.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + }, + }, + test: { + pool: 'forks', + poolOptions: { + forks: { + maxForks: 4, + }, + }, + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/pickup-in-store`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-pickup-in-store.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/product-configurator/common/components/configurator-cart-entry-bundle-info/configurator-cart-entry-bundle-info.component.spec.ts b/feature-libs/product-configurator/common/components/configurator-cart-entry-bundle-info/configurator-cart-entry-bundle-info.component.spec.ts index 60f50fe4c10..9635b0812e6 100644 --- a/feature-libs/product-configurator/common/components/configurator-cart-entry-bundle-info/configurator-cart-entry-bundle-info.component.spec.ts +++ b/feature-libs/product-configurator/common/components/configurator-cart-entry-bundle-info/configurator-cart-entry-bundle-info.component.spec.ts @@ -6,7 +6,7 @@ import { PipeTransform, Type, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ControlContainer, UntypedFormControl } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { @@ -35,6 +35,7 @@ import { BehaviorSubject, EMPTY, of, ReplaySubject } from 'rxjs'; import { take, toArray } from 'rxjs/operators'; import { CommonConfiguratorTestUtilsService } from '../../testing/common-configurator-test-utils.service'; import { ConfiguratorCartEntryBundleInfoComponent } from './configurator-cart-entry-bundle-info.component'; +import { vi } from 'vitest'; @Pipe({ name: 'cxNumeric' }) class MockNumericPipe implements PipeTransform { @@ -147,14 +148,8 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { ConfiguratorCartEntryBundleInfoService as Type ); - spyOn( - commonConfigUtilsService, - 'isBundleBasedConfigurator' - ).and.callThrough(); - spyOn( - configCartEntryBundleInfoService, - 'retrieveLineItems' - ).and.callThrough(); + vi.spyOn(commonConfigUtilsService, 'isBundleBasedConfigurator'); + vi.spyOn(configCartEntryBundleInfoService, 'retrieveLineItems'); breakpointService = TestBed.inject( BreakpointService as Type @@ -165,38 +160,37 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { component = fixture.componentInstance; htmlElem = fixture.nativeElement; mockCartItemContext = TestBed.inject(CartItemContext) as any; - - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); - it('should expose orderEntry$', (done) => { + it('should expose orderEntry$', async () => { + fixture.detectChanges(); const orderEntry: OrderEntry = { orderCode: '123' }; component.orderEntry$.pipe(take(1)).subscribe((value) => { expect(value).toBe(orderEntry); - done(); }); mockCartItemContext.item$.next(orderEntry); }); - it('should expose quantityControl$', (done) => { + it('should expose quantityControl$', async () => { + fixture.detectChanges(); const quantityControl = new UntypedFormControl(); component.quantityControl$.pipe(take(1)).subscribe((value) => { expect(value).toBe(quantityControl); - done(); }); mockCartItemContext.quantityControl$.next(quantityControl); }); - it('should expose readonly$', (done) => { + it('should expose readonly$', async () => { + fixture.detectChanges(); component.readonly$.pipe(take(2), toArray()).subscribe((values) => { expect(values).toEqual([true, false]); - done(); }); mockCartItemContext.readonly$.next(true); @@ -270,6 +264,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { describe('toggleItems', () => { it('should return corresponding state after toggling the link show / hide items', () => { + fixture.detectChanges(); expect(component.hideItems).toBe(true); component.toggleItems(); expect(component.hideItems).toBe(false); @@ -369,10 +364,9 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { fixture.detectChanges(); }); - it('should display number of bundle items', (done) => { + it('should display number of bundle items', async () => { component.numberOfLineItems$.subscribe((numberOfItems) => { expect(numberOfItems).toBe(3); - done(); }); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -449,7 +443,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { }); it('should display', () => { - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -545,7 +539,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { }); it('should display', () => { - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -710,6 +704,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { describe('getItemsMsg', () => { it("should return 'configurator.a11y.cartEntryBundleInfo' if there is only one line item", () => { + fixture.detectChanges(); let numberOfItems: number = 1; expect( component @@ -719,6 +714,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { }); it("should return 'configurator.a11y.cartEntryBundleInfo_other' if there are more than one line item", () => { + fixture.detectChanges(); let numberOfItems: number = 4; expect( component @@ -730,6 +726,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { describe('getHiddenItemInfo', () => { it("should return 'configurator.a11y.cartEntryBundleInfo' if the item name, price and quantity are defined", () => { + fixture.detectChanges(); let lineItem: LineItem = { name: 'Canon ABC', formattedPrice: '$1,000.00', @@ -743,6 +740,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { }); it("should return 'configurator.a11y.cartEntryBundleNameWithPrice' if the item name and price are defined", () => { + fixture.detectChanges(); let lineItem: LineItem = { name: 'Canon ABC', formattedPrice: '$1,000.00', @@ -755,6 +753,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { }); it("should return 'configurator.a11y.cartEntryBundleNameWithQuantity' if the item name and quantity are defined", () => { + fixture.detectChanges(); let lineItem: LineItem = { name: 'Canon ABC', formattedQuantity: '5', @@ -767,6 +766,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { }); it("should return 'configurator.a11y.cartEntryBundleName' if only item name is defined", () => { + fixture.detectChanges(); let lineItem: LineItem = { name: 'Canon ABC', }; @@ -797,7 +797,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { mockCartItemContext.readonly$.next(false); mockCartItemContext.quantityControl$.next(new UntypedFormControl()); component.hideItems = false; - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); fixture.detectChanges(); }); @@ -928,6 +928,7 @@ describe('ConfiguratorCartEntryBundleInfoComponent', () => { describe('getHiddenItemInfoId', () => { it("should return 'cx-item-hidden-info-4' ID for a corresponding line item", () => { + fixture.detectChanges(); expect( component.getHiddenItemInfoId(4).indexOf('cx-item-hidden-info-4') ).toBe(0); @@ -940,11 +941,11 @@ describe('ConfiguratorCartEntryBundleInfoComponent without cart item context', ( let component: ConfiguratorCartEntryBundleInfoComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule, ConfiguratorCartEntryBundleInfoComponent], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorCartEntryBundleInfoComponent); diff --git a/feature-libs/product-configurator/common/components/configurator-cart-entry-info/configurator-cart-entry-info.component.spec.ts b/feature-libs/product-configurator/common/components/configurator-cart-entry-info/configurator-cart-entry-info.component.spec.ts index 8c01cd7d2d7..16c129ae472 100644 --- a/feature-libs/product-configurator/common/components/configurator-cart-entry-info/configurator-cart-entry-info.component.spec.ts +++ b/feature-libs/product-configurator/common/components/configurator-cart-entry-info/configurator-cart-entry-info.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ControlContainer, ReactiveFormsModule, @@ -52,7 +52,7 @@ describe('ConfiguratorCartEntryInfoComponent', () => { let htmlElem: HTMLElement; let mockCartItemContext: MockCartItemContext; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, ConfiguratorCartEntryInfoComponent], providers: [ @@ -75,45 +75,44 @@ describe('ConfiguratorCartEntryInfoComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorCartEntryInfoComponent); component = fixture.componentInstance; htmlElem = fixture.nativeElement; mockCartItemContext = TestBed.inject(CartItemContext) as any; - - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); - it('should expose orderEntry$', (done) => { + it('should expose orderEntry$', async () => { + fixture.detectChanges(); const orderEntry: OrderEntry = { orderCode: '123' }; component.orderEntry$.pipe(take(1)).subscribe((value) => { expect(value).toBe(orderEntry); - done(); }); mockCartItemContext.item$.next(orderEntry); }); - it('should expose quantityControl$', (done) => { + it('should expose quantityControl$', async () => { + fixture.detectChanges(); const quantityControl = new UntypedFormControl(); component.quantityControl$.pipe(take(1)).subscribe((value) => { expect(value).toBe(quantityControl); - done(); }); mockCartItemContext.quantityControl$.next(quantityControl); }); - it('should expose readonly$', (done) => { + it('should expose readonly$', async () => { + fixture.detectChanges(); component.readonly$.pipe(take(2), toArray()).subscribe((values) => { expect(values).toEqual([true, false]); - done(); }); mockCartItemContext.readonly$.next(true); mockCartItemContext.readonly$.next(false); @@ -127,6 +126,7 @@ describe('ConfiguratorCartEntryInfoComponent', () => { }); mockCartItemContext.readonly$.next(false); + fixture.detectChanges(); const htmlElementAfterChanges = fixture.nativeElement; expect( htmlElementAfterChanges.querySelectorAll('.cx-configuration-info') @@ -178,6 +178,7 @@ describe('ConfiguratorCartEntryInfoComponent', () => { describe('hasStatus', () => { it('should be true if first entry of status summary is in error status and has a definition of the configurator type', () => { + fixture.detectChanges(); const entry: OrderEntry = { configurationInfos: [ { status: 'ERROR', configuratorType: ConfiguratorType.VARIANT }, @@ -187,16 +188,19 @@ describe('ConfiguratorCartEntryInfoComponent', () => { }); it('should be false if first entry of status summary carries no status', () => { + fixture.detectChanges(); const entry: OrderEntry = { configurationInfos: [{ status: 'NONE' }] }; expect(component.hasStatus(entry)).toBe(false); }); it('should be false if no configuration infos are present', () => { + fixture.detectChanges(); const entry: OrderEntry = {}; expect(component.hasStatus(entry)).toBe(false); }); it('should be false if configuration infos are empty', () => { + fixture.detectChanges(); const entry: OrderEntry = { configurationInfos: [] }; expect(component.hasStatus(entry)).toBe(false); }); @@ -204,6 +208,7 @@ describe('ConfiguratorCartEntryInfoComponent', () => { describe('isAttributeBasedConfigurator', () => { it('should return true if for CCP based configurator', () => { + fixture.detectChanges(); const entry: OrderEntry = { configurationInfos: [ { status: 'ERROR', configuratorType: ConfiguratorType.VARIANT }, @@ -212,6 +217,7 @@ describe('ConfiguratorCartEntryInfoComponent', () => { expect(component.isAttributeBasedConfigurator(entry)).toBe(true); }); it('should return false if no configurationInfos are provided', () => { + fixture.detectChanges(); const entry: OrderEntry = {}; expect(component.isAttributeBasedConfigurator(entry)).toBe(false); }); @@ -390,11 +396,11 @@ describe('ConfiguratorCartEntryInfoComponent without cart item context', () => { let component: ConfiguratorCartEntryInfoComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorCartEntryInfoComponent], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorCartEntryInfoComponent); diff --git a/feature-libs/product-configurator/common/components/configurator-issues-notification/configurator-issues-notification.component.spec.ts b/feature-libs/product-configurator/common/components/configurator-issues-notification/configurator-issues-notification.component.spec.ts index e9942de5ea8..77c546e1a59 100644 --- a/feature-libs/product-configurator/common/components/configurator-issues-notification/configurator-issues-notification.component.spec.ts +++ b/feature-libs/product-configurator/common/components/configurator-issues-notification/configurator-issues-notification.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UntypedFormControl } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { CartItemContextSource } from '@spartacus/cart/base/components'; @@ -76,7 +76,7 @@ describe('ConfigureIssuesNotificationComponent', () => { mockCartItemContext.quantityControl$?.next(new UntypedFormControl()); } describe('with cart item context', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorIssuesNotificationComponent], providers: [ @@ -100,7 +100,7 @@ describe('ConfigureIssuesNotificationComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -109,39 +109,38 @@ describe('ConfigureIssuesNotificationComponent', () => { component = fixture.componentInstance; htmlElem = fixture.nativeElement; mockCartItemContext = TestBed.inject(CartItemContext) as any; - - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); - it('should expose orderEntry$', (done) => { + it('should expose orderEntry$', async () => { + fixture.detectChanges(); const orderEntry: OrderEntry = { orderCode: '123' }; component.orderEntry$.pipe(take(1)).subscribe((value) => { expect(value).toBe(orderEntry); - done(); }); mockCartItemContext.item$?.next(orderEntry); }); - it('should expose quantityControl$', (done) => { + it('should expose quantityControl$', async () => { + fixture.detectChanges(); const quantityControl = new UntypedFormControl(); component.quantityControl$.pipe(take(1)).subscribe((value) => { expect(value).toBe(quantityControl); - done(); }); mockCartItemContext.quantityControl$?.next(quantityControl); }); - it('should expose readonly$', (done) => { + it('should expose readonly$', async () => { + fixture.detectChanges(); component.readonly$.pipe(take(2), toArray()).subscribe((values) => { expect(values).toEqual([true, false]); - done(); }); mockCartItemContext.readonly$?.next(true); @@ -336,7 +335,7 @@ describe('ConfigureIssuesNotificationComponent', () => { }); }); describe('without cart item context', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorIssuesNotificationComponent], providers: [{ provide: CartItemContext, useValue: null }], @@ -358,7 +357,7 @@ describe('ConfigureIssuesNotificationComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( diff --git a/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts b/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts index 0c74da2a7c0..2bbca102cd7 100644 --- a/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts +++ b/feature-libs/product-configurator/common/components/configure-cart-entry/configure-cart-entry.component.spec.ts @@ -185,22 +185,20 @@ describe('ConfigureCartEntryComponent', () => { configureTestingModule().compileComponents(); assignTestArtifacts(); }); - it('should return false in case the url does not contain checkoutReviewOrder', (done) => { + it('should return false in case the url does not contain checkoutReviewOrder', async () => { component['isInCheckout']() .pipe(take(1), delay(0)) .subscribe((isInCheckout) => { expect(isInCheckout).toBe(false); - done(); }); }); - it('should return true in case the url contains checkoutReviewOrder in case one comes from the checkout', (done) => { + it('should return true in case the url contains checkoutReviewOrder in case one comes from the checkout', async () => { mockRouterState.state.semanticRoute = 'checkoutReviewOrder'; component['isInCheckout']() .pipe(take(1), delay(0)) .subscribe((isInCheckout) => { expect(isInCheckout).toBe(true); - done(); }); }); }); @@ -459,17 +457,16 @@ describe('ConfigureCartEntryComponent', () => { }); describe('queryParam$', () => { - it('should contain "navigateToCheckout" parameter in case the navigation to the cart is relevant', (done) => { + it('should contain "navigateToCheckout" parameter in case the navigation to the cart is relevant', async () => { mockRouterState.state.semanticRoute = 'checkoutReviewOrder'; component.queryParams$ .pipe(take(1), delay(0)) .subscribe((queryParams) => { expect(queryParams.navigateToCheckout).toBe(true); - done(); }); }); - it('should contain "productCode" parameter in case product code is relevant', (done) => { + it('should contain "productCode" parameter in case product code is relevant', async () => { component.cartEntry = { entryNumber: 0, product: { configuratorType: configuratorType, code: productCode }, @@ -479,11 +476,10 @@ describe('ConfigureCartEntryComponent', () => { .pipe(take(1), delay(0)) .subscribe((queryParams) => { expect(queryParams.productCode).toBe(productCode); - done(); }); }); - it('should not contain "resolveIssues" parameter in case no issues exist', (done) => { + it('should not contain "resolveIssues" parameter in case no issues exist', async () => { component.readOnly = false; component.msgBanner = false; component.cartEntry = { @@ -495,11 +491,10 @@ describe('ConfigureCartEntryComponent', () => { .pipe(take(1), delay(0)) .subscribe((queryParams) => { expect(queryParams.resolveIssues).toBe(false); - done(); }); }); - it('should contain "resolveIssues" parameter in case issues exist', (done) => { + it('should contain "resolveIssues" parameter in case issues exist', async () => { component.readOnly = false; component.msgBanner = true; component.cartEntry = { @@ -514,7 +509,6 @@ describe('ConfigureCartEntryComponent', () => { .pipe(take(1), delay(0)) .subscribe((queryParams) => { expect(queryParams.resolveIssues).toBe(true); - done(); }); }); }); diff --git a/feature-libs/product-configurator/common/components/configure-product/configure-product.component.spec.ts b/feature-libs/product-configurator/common/components/configure-product/configure-product.component.spec.ts index dfafb034c0d..858c65d3e27 100644 --- a/feature-libs/product-configurator/common/components/configure-product/configure-product.component.spec.ts +++ b/feature-libs/product-configurator/common/components/configure-product/configure-product.component.spec.ts @@ -22,6 +22,7 @@ import { ReadOnlyPostfix, } from './../../core/model/common-configurator.model'; import { ConfigureProductComponent } from './configure-product.component'; +import { vi } from 'vitest'; const productCode = 'CONF_LAPTOP'; const configuratorType = ConfiguratorType.VARIANT; @@ -165,7 +166,7 @@ function setupWithCurrentProductService( ); routingService = TestBed.inject(RoutingService); - spyOn(currentProductService, 'getProduct').and.callThrough(); + vi.spyOn(currentProductService, 'getProduct'); fixture = TestBed.createComponent(ConfigureProductComponent); component = fixture.componentInstance; @@ -208,22 +209,20 @@ describe('ConfigureProductComponent', () => { ); }); - it('should emit product in case it was launched with current product service', (done) => { + it('should emit product in case it was launched with current product service', async () => { setupWithCurrentProductService(true); component.product$.subscribe((product) => { expect(product).toBe(mockProduct); - done(); }); }); - it('should emit non-configurable dummy in case it was launched with product service which emits null', (done) => { + it('should emit non-configurable dummy in case it was launched with product service which emits null', async () => { setupWithCurrentProductService(true, true, true); component['productListItemContext'] = null; component['currentProductService'] = null; fixture.detectChanges(); component.product$.subscribe((product) => { expect(product).toEqual(mockProductNotConfigurable); - done(); }); }); @@ -237,40 +236,36 @@ describe('ConfigureProductComponent', () => { ); }); - it('should emit product in case it was launched with product item context', (done) => { + it('should emit product in case it was launched with product item context', async () => { setupWithCurrentProductService(false); component.product$.subscribe((product) => { expect(product).toBe(mockProduct); - done(); }); }); describe('getProduct', () => { - it('should emit null in case both productListItemContext and currentProductService return null', (done) => { + it('should emit null in case both productListItemContext and currentProductService return null', async () => { setupWithCurrentProductService(true, true, true); component['getProduct']().subscribe((product) => { expect(product).toBe(null); - done(); }); }); - it('should emit product in case it was launched with defined product item context', (done) => { + it('should emit product in case it was launched with defined product item context', async () => { setupWithCurrentProductService(false); component['getProduct']().subscribe((product) => { expect(product).toBe(mockProduct); - done(); }); }); - it('should emit product in case it was launched with defined currentProductService', (done) => { + it('should emit product in case it was launched with defined currentProductService', async () => { setupWithCurrentProductService(true); component['getProduct']().subscribe((product) => { expect(product).toBe(mockProduct); - done(); }); }); - it('should emit null in case both productListItemContext and currentProductService are undefined', (done) => { + it('should emit null in case both productListItemContext and currentProductService are undefined', async () => { setupWithCurrentProductService(true); component['productListItemContext'] = null; component['currentProductService'] = null; @@ -278,7 +273,6 @@ describe('ConfigureProductComponent', () => { component['getProduct']().subscribe((product) => { expect(product).toBe(null); - done(); }); }); }); @@ -446,7 +440,7 @@ describe('ConfigureProductComponent', () => { it('should navigate to a product configurator', () => { setupWithCurrentProductService(true); fixture.detectChanges(); - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); const btn = fixture.debugElement.query(By.css('button')); btn.triggerEventHandler('click'); expect(routingService.go).toHaveBeenCalledWith( diff --git a/feature-libs/product-configurator/common/components/service/configurator-router-extractor.service.spec.ts b/feature-libs/product-configurator/common/components/service/configurator-router-extractor.service.spec.ts index 37462b155de..85fa8772bd2 100644 --- a/feature-libs/product-configurator/common/components/service/configurator-router-extractor.service.spec.ts +++ b/feature-libs/product-configurator/common/components/service/configurator-router-extractor.service.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { I18nTestingModule, RouterState, @@ -29,7 +29,7 @@ class MockRoutingService { describe('ConfigRouterExtractorService', () => { let serviceUnderTest: ConfiguratorRouterExtractorService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule], providers: [ @@ -39,7 +39,7 @@ describe('ConfigRouterExtractorService', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { serviceUnderTest = TestBed.inject( ConfiguratorRouterExtractorService as Type @@ -162,7 +162,7 @@ describe('ConfigRouterExtractorService', () => { .unsubscribe(); }); - it('should tell from the URL if we need to resolve issues without ignoring conflicts of a configuration', (done) => { + it('should tell from the URL if we need to resolve issues without ignoring conflicts of a configuration', async () => { mockRouterState.state.queryParams = { resolveIssues: 'true' }; let routerData: ConfiguratorRouter.Data; serviceUnderTest @@ -171,12 +171,11 @@ describe('ConfigRouterExtractorService', () => { routerData = data; expect(routerData.resolveIssues).toBe(true); expect(routerData.skipConflicts).toBe(false); - done(); }) .unsubscribe(); }); - it('should tell from the URL if we need to skip conflicts while resolving issues of a configuration', (done) => { + it('should tell from the URL if we need to skip conflicts while resolving issues of a configuration', async () => { mockRouterState.state.queryParams = { resolveIssues: 'true', skipConflicts: 'true', @@ -188,7 +187,6 @@ describe('ConfigRouterExtractorService', () => { routerData = data; expect(routerData.resolveIssues).toBe(true); expect(routerData.skipConflicts).toBe(true); - done(); }) .unsubscribe(); }); diff --git a/feature-libs/product-configurator/common/shared/utils/common-configurator-utils.service.spec.ts b/feature-libs/product-configurator/common/shared/utils/common-configurator-utils.service.spec.ts index 143e898c836..0538c1b3346 100644 --- a/feature-libs/product-configurator/common/shared/utils/common-configurator-utils.service.spec.ts +++ b/feature-libs/product-configurator/common/shared/utils/common-configurator-utils.service.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { UntypedFormControl } from '@angular/forms'; import { CartItemContextSource } from '@spartacus/cart/base/components'; import { @@ -76,7 +76,7 @@ describe('CommonConfiguratorUtilsService', () => { let classUnderTest: CommonConfiguratorUtilsService; let mockCartItemContext: CartItemContextSource; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ { @@ -86,7 +86,7 @@ describe('CommonConfiguratorUtilsService', () => { { provide: CartItemContext, useClass: MockCartItemContext }, ], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( CommonConfiguratorUtilsService as Type diff --git a/feature-libs/product-configurator/karma.conf.js b/feature-libs/product-configurator/karma.conf.js deleted file mode 100644 index 359cada4644..00000000000 --- a/feature-libs/product-configurator/karma.conf.js +++ /dev/null @@ -1,55 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-product-configurator.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join( - __dirname, - '../../coverage/product-configurator' - ), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 75, - functions: 90, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/product-configurator/project.json b/feature-libs/product-configurator/project.json index 491c0e3a75d..2c0cf1422af 100644 --- a/feature-libs/product-configurator/project.json +++ b/feature-libs/product-configurator/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/product-configurator/test.ts", - "tsConfig": "feature-libs/product-configurator/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/product-configurator/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/product-configurator/rulebased/components/add-to-cart-button/configurator-add-to-cart-button.component.spec.ts b/feature-libs/product-configurator/rulebased/components/add-to-cart-button/configurator-add-to-cart-button.component.spec.ts index a8b5b2a2b88..ffa40eff8a9 100644 --- a/feature-libs/product-configurator/rulebased/components/add-to-cart-button/configurator-add-to-cart-button.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/add-to-cart-button/configurator-add-to-cart-button.component.spec.ts @@ -1,11 +1,5 @@ import { Component, Input, Type } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UntypedFormControl } from '@angular/forms'; import { ActiveCartFacade, @@ -36,7 +30,7 @@ import { KeyboardFocusService, } from '@spartacus/storefront'; import { MockFeatureLevelDirective } from 'core-libs/storefront/shared/test/mock-feature-level-directive'; -import { Observable, of } from 'rxjs'; +import { firstValueFrom, Observable, of } from 'rxjs'; import { delay, take } from 'rxjs/operators'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorCartService } from '../../core/facade/configurator-cart.service'; @@ -47,7 +41,7 @@ import { ConfiguratorQuantityService } from '../../core/services/configurator-qu import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorAddToCartButtonComponent } from './configurator-add-to-cart-button.component'; -import createSpy = jasmine.createSpy; +import { vi } from 'vitest'; const CART_ENTRY_KEY = '001+1'; const ORDER_ENTRY_KEY = '002+1'; @@ -142,7 +136,6 @@ function initialize() { component = fixture.componentInstance; htmlElem = fixture.nativeElement; component.quantityControl = new UntypedFormControl(1); - fixture.detectChanges(); } function initTestData() { @@ -372,6 +365,7 @@ function performAddToCartOnOverview() { mockRouterData.pageType = ConfiguratorRouter.PageType.OVERVIEW; mockRouterData.productCode = mockProductConfiguration.productCode; initialize(); + fixture.detectChanges(); component.onAddToCart(mockProductConfiguration, mockRouterData); } @@ -384,6 +378,7 @@ function ensureCartBound() { setRouterTestDataCartBoundAndConfigPage(); mockOwner.id = CART_ENTRY_KEY; initialize(); + fixture.detectChanges(); } function ensureCartBoundAndOnOverview() { @@ -391,6 +386,7 @@ function ensureCartBoundAndOnOverview() { mockRouterState.state.semanticRoute = ROUTE_OVERVIEW; mockRouterData.pageType = ConfiguratorRouter.PageType.OVERVIEW; initialize(); + fixture.detectChanges(); } function ensureProductBound() { @@ -399,6 +395,7 @@ function ensureProductBound() { mockProductConfiguration.nextOwner.id = CART_ENTRY_KEY; } initialize(); + fixture.detectChanges(); } function performUpdateOnOV() { @@ -419,7 +416,7 @@ class MockConfiguratorAddToCartButtonComponent { } class MockActiveCartFacade implements Partial { - getActive = createSpy().and.returnValue(of(cart)); + getActive = vi.fn().mockReturnValue(of(cart)); } class MockConfiguratorStorefrontUtilsService { @@ -439,18 +436,18 @@ describe('ConfiguratorAddToCartButtonComponent', () => { let configuratorQuantityService: ConfiguratorQuantityService; let keyboardFocusService: KeyboardFocusService; - function checkNavigationFlow() { - tick(); + async function checkNavigationFlow() { + await vi.advanceTimersByTimeAsync(0); expect(routingService.go).toHaveBeenCalledTimes(2); - const allArgs = (routingService.go as jasmine.Spy).calls.allArgs(); + const allArgs = vi.mocked(routingService.go).mock.calls; expect(allArgs[0][0]).toEqual(navParamsConfig); expect(allArgs[0][1]).toEqual(replaceUrlParam); expect(allArgs[1][0]).toEqual(navParamsOverview); expect(allArgs[1][1]).toEqual(queryParams); } - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorAddToCartButtonComponent, I18nTestingModule], providers: [ @@ -519,7 +516,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { initTestData(); @@ -536,25 +533,27 @@ describe('ConfiguratorAddToCartButtonComponent', () => { intersectionService = TestBed.inject(IntersectionService); keyboardFocusService = TestBed.inject(KeyboardFocusService); - spyOn(configuratorGroupsService, 'setGroupStatusVisited').and.callThrough(); - spyOn(routingService, 'go').and.callThrough(); - spyOn(globalMessageService, 'add').and.callThrough(); - spyOn(configuratorCommonsService, 'removeConfiguration').and.callThrough(); - spyOn(configuratorQuantityService, 'setQuantity').and.callThrough(); + vi.spyOn(configuratorGroupsService, 'setGroupStatusVisited'); + vi.spyOn(routingService, 'go'); + vi.spyOn(globalMessageService, 'add'); + vi.spyOn(configuratorCommonsService, 'removeConfiguration'); + vi.spyOn(configuratorQuantityService, 'setQuantity'); configuratorCartService = TestBed.inject( ConfiguratorCartService as Type ); - spyOn(configuratorCartService, 'getEntry').and.callThrough(); - spyOn(configuratorStorefrontUtilsService, 'changeStyling').and.stub(); - spyOn( + vi.spyOn(configuratorCartService, 'getEntry'); + vi.spyOn( configuratorStorefrontUtilsService, - 'focusFirstActiveElement' - ).and.callThrough(); - spyOn(keyboardFocusService, 'clear').and.callThrough(); + 'changeStyling' + ).mockImplementation(() => {}); + vi.spyOn(configuratorStorefrontUtilsService, 'focusFirstActiveElement'); + vi.spyOn(keyboardFocusService, 'clear'); + fixture.detectChanges(); }); it('should create cart-btn-container', () => { initialize(); + fixture.detectChanges(); expect(component).toBeTruthy(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -584,6 +583,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should create display-only-btn-container', () => { setRouterTestDataReadOnlyOrder(); initialize(); + fixture.detectChanges(); expect(component).toBeTruthy(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -607,6 +607,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should render button that is not disabled in case there are no pending changes', () => { initialize(); + fixture.detectChanges(); const selector = htmlElem.querySelector('button'); if (selector) { expect(selector.disabled).toBe(false); @@ -618,6 +619,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should not disable button in case there are pending changes', () => { pendingChangesObservable = of(true); initialize(); + fixture.detectChanges(); const selector = htmlElem.querySelector('button'); if (selector) { expect(selector.disabled).toBe(false); @@ -629,6 +631,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { describe('ngOnInit', () => { it('should set quantity that was retrieved from quantity service', () => { initialize(); + fixture.detectChanges(); expect(component.quantityControl.value).toBe(QUANTITY); }); }); @@ -636,6 +639,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { describe('quantityChange', () => { it('should push current quantity to qty service', () => { initialize(); + fixture.detectChanges(); component.quantityControl.setValue(QUANTITY_CHANGED); expect(configuratorQuantityService.setQuantity).toHaveBeenCalledWith( QUANTITY_CHANGED @@ -644,16 +648,22 @@ describe('ConfiguratorAddToCartButtonComponent', () => { }); describe('onAddToCart', () => { - it('should navigate to OV in case configuration is cart bound and we are on product config page', fakeAsync(() => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should navigate to OV in case configuration is cart bound and we are on product config page', async () => { mockRouterData.pageType = ConfiguratorRouter.PageType.CONFIGURATION; performUpdateCart(); - checkNavigationFlow(); + await checkNavigationFlow(); expect( configuratorGroupsService.setGroupStatusVisited ).toHaveBeenCalled(); - })); + }); it('should navigate to cart in case configuration is cart bound and we are on OV config page', () => { performUpdateOnOV(); @@ -692,15 +702,15 @@ describe('ConfiguratorAddToCartButtonComponent', () => { expect(globalMessageService.add).toHaveBeenCalledTimes(1); }); - it('should navigate to overview in case configuration has not been added yet and we are on configuration page', fakeAsync(() => { + it('should navigate to overview in case configuration has not been added yet and we are on configuration page', async () => { ensureProductBound(); component.onAddToCart(mockProductConfiguration, mockRouterData); - checkNavigationFlow(); + await checkNavigationFlow(); expect( configuratorGroupsService.setGroupStatusVisited ).toHaveBeenCalled(); - })); + }); it('should remove one configuration (cart bound) in case configuration has not yet been added and we are on configuration page', () => { ensureProductBound(); @@ -739,7 +749,13 @@ describe('ConfiguratorAddToCartButtonComponent', () => { }); describe('navigateForProductBound', () => { - it('should navigate to OV in case configuration is product bound and we are on product config page', fakeAsync(() => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should navigate to OV in case configuration is product bound and we are on product config page', async () => { mockRouterData.pageType = ConfiguratorRouter.PageType.CONFIGURATION; ensureProductBound(); @@ -749,10 +765,10 @@ describe('ConfiguratorAddToCartButtonComponent', () => { false, mockProductConfiguration.productCode ); - checkNavigationFlow(); - })); + await checkNavigationFlow(); + }); - it('should handle case that next owner is not defined', fakeAsync(() => { + it('should handle case that next owner is not defined', async () => { mockRouterData.pageType = ConfiguratorRouter.PageType.CONFIGURATION; ensureProductBound(); @@ -763,10 +779,10 @@ describe('ConfiguratorAddToCartButtonComponent', () => { mockProductConfiguration.productCode ); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(routingService.go).toHaveBeenCalledTimes(2); - const allArgs = (routingService.go as jasmine.Spy).calls.allArgs(); + const allArgs = vi.mocked(routingService.go).mock.calls; expect(allArgs[0][0]).toEqual({ ...navParamsConfig, params: { ...navParamsConfig.params, entityKey: 'INITIAL' }, @@ -777,7 +793,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { params: { ...navParamsOverview.params, entityKey: 'INITIAL' }, }); expect(allArgs[1][1]).toEqual(queryParams); - })); + }); }); describe('performNavigation', () => { @@ -807,6 +823,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should navigate to order details', () => { setRouterTestDataReadOnlyOrder(); initialize(); + fixture.detectChanges(); component.leaveConfigurationOverview(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'orderDetails', @@ -817,6 +834,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should navigate to quote details in case owner is quote entry', () => { setRouterTestDataReadOnlySavedCart(); initialize(); + fixture.detectChanges(); component.leaveConfigurationOverview(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'quoteDetails', @@ -827,6 +845,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should navigate to quote details in case owner is saved cart entry and saved cart is bound to a quote', () => { setRouterTestDataReadOnlyQuote(); initialize(); + fixture.detectChanges(); component.leaveConfigurationOverview(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'quoteDetails', @@ -837,6 +856,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should navigate to product details', () => { setRouterTestDataReadOnlyProduct(); initialize(); + fixture.detectChanges(); component.leaveConfigurationOverview(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'product', @@ -849,6 +869,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should navigate to cart', () => { setRouterTestDataReadOnlyCart(); initialize(); + fixture.detectChanges(); component.leaveConfigurationOverview(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'cart', @@ -858,6 +879,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { it('should navigate to checkout review order', () => { setRouterTestDataReadOnlyCheckout(); initialize(); + fixture.detectChanges(); component.leaveConfigurationOverview(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'checkoutReviewOrder', @@ -866,40 +888,38 @@ describe('ConfiguratorAddToCartButtonComponent', () => { }); describe('Floating button', () => { - it('should make button sticky', (done) => { - spyOn(configuratorStorefrontUtilsService, 'getElement').and.returnValue( - elementMock as unknown as HTMLElement - ); - spyOn(intersectionService, 'isIntersecting').and.returnValue(of(true)); + it('should make button sticky', async () => { + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElement' + ).mockReturnValue(elementMock as unknown as HTMLElement); + vi.spyOn(intersectionService, 'isIntersecting').mockReturnValue(of(true)); component.ngOnInit(); - component.container$.pipe(take(1), delay(0)).subscribe(() => { - expect( - configuratorStorefrontUtilsService.changeStyling - ).toHaveBeenCalledWith( - 'cx-configurator-add-to-cart-button', - 'position', - 'sticky' - ); - done(); - }); + await firstValueFrom(component.container$.pipe(delay(0))); + expect( + configuratorStorefrontUtilsService.changeStyling + ).toHaveBeenCalledWith( + 'cx-configurator-add-to-cart-button', + 'position', + 'sticky' + ); }); - it('should make button fixed when not intersecting', (done) => { - spyOn(configuratorStorefrontUtilsService, 'getElement').and.returnValue( - elementMock as unknown as HTMLElement - ); + it('should make button fixed when not intersecting', async () => { + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElement' + ).mockReturnValue(elementMock as unknown as HTMLElement); component.ngOnInit(); - component.container$.pipe(take(1), delay(0)).subscribe(() => { - spyOn(intersectionService, 'isIntersecting').and.callThrough(); - expect( - configuratorStorefrontUtilsService.changeStyling - ).toHaveBeenCalledWith( - 'cx-configurator-add-to-cart-button', - 'position', - 'fixed' - ); - done(); - }); + await firstValueFrom(component.container$.pipe(delay(0))); + vi.spyOn(intersectionService, 'isIntersecting'); + expect( + configuratorStorefrontUtilsService.changeStyling + ).toHaveBeenCalledWith( + 'cx-configurator-add-to-cart-button', + 'position', + 'fixed' + ); }); }); @@ -1101,34 +1121,40 @@ describe('ConfiguratorAddToCartButtonComponent', () => { }); describe('Focus handling on navigation', () => { - it('focusOverviewInTabBar should call clear and focusFirstActiveElement', fakeAsync(() => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it('focusOverviewInTabBar should call clear and focusFirstActiveElement', async () => { component['focusOverviewInTabBar'](); - tick(1); // needed because of delay(0) in focusOverviewInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusOverviewInTabBar expect(keyboardFocusService.clear).toHaveBeenCalledTimes(1); expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(1); - })); + }); - it('focusOverviewInTabBar should not call clear and focusFirstActiveElement if overview data is not present in configuration', fakeAsync(() => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('focusOverviewInTabBar should not call clear and focusFirstActiveElement if overview data is not present in configuration', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(mockProductConfigurationWithoutBasePrice) ); component['focusOverviewInTabBar'](); - tick(1); // needed because of delay(0) in focusOverviewInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusOverviewInTabBar expect(keyboardFocusService.clear).toHaveBeenCalledTimes(0); expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(0); - })); + }); - it('navigateToOverview should navigate to overview page and should call focusFirstActiveElement inside focusOverviewInTabBar', fakeAsync(() => { + it('navigateToOverview should navigate to overview page and should call focusFirstActiveElement inside focusOverviewInTabBar', async () => { component['navigateToOverview']( mockRouterData.owner.configuratorType, mockRouterData.owner, mockProductConfiguration.productCode ); - tick(1); // needed because of delay(0) in focusOverviewInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusOverviewInTabBar expect(routingService.go).toHaveBeenCalledWith( { cxRoute: 'configureOverview' + mockRouterData.owner.configuratorType, @@ -1142,7 +1168,7 @@ describe('ConfiguratorAddToCartButtonComponent', () => { expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(1); - })); + }); }); describe('isQuoteCartActive', () => { diff --git a/feature-libs/product-configurator/rulebased/components/attribute/composition/configurator-attribute-composition.directive.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/composition/configurator-attribute-composition.directive.spec.ts index f72b6ab7727..061eeb511aa 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/composition/configurator-attribute-composition.directive.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/composition/configurator-attribute-composition.directive.spec.ts @@ -4,13 +4,13 @@ import { LoggerService } from '@spartacus/core'; import { ConfiguratorTestUtils } from '../../../testing/configurator-test-utils'; import { ConfiguratorAttributeCompositionConfig } from './configurator-attribute-composition.config'; import { ConfiguratorAttributeCompositionDirective } from './configurator-attribute-composition.directive'; -import createSpy = jasmine.createSpy; +import { vi } from 'vitest'; class TestComponent {} class MockViewContainerRef { - clear = createSpy('vcr.clear'); - createComponent = createSpy('vcr.createComponent'); + clear = vi.fn(); + createComponent = vi.fn(); } describe('ConfiguratorAttributeCompositionDirective', () => { @@ -26,7 +26,7 @@ describe('ConfiguratorAttributeCompositionDirective', () => { ViewContainerRef as Type ); loggerService = TestBed.inject(LoggerService as Type); - spyOn(loggerService, 'warn').and.callThrough(); + vi.spyOn(loggerService, 'warn'); classUnderTest['context'] = ConfiguratorTestUtils.getAttributeContext(); } diff --git a/feature-libs/product-configurator/rulebased/components/attribute/footer/configurator-attribute-footer.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/footer/configurator-attribute-footer.component.spec.ts index 5a87230dbba..d06fc0e24d6 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/footer/configurator-attribute-footer.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/footer/configurator-attribute-footer.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { CommonConfigurator, @@ -57,7 +57,6 @@ function createComponentWithData( component.attribute.uiType = Configurator.UiType.STRING; component.attribute.userInput = ''; - fixture.detectChanges(); return component; } @@ -79,7 +78,7 @@ const owner = ConfiguratorModelUtils.createOwner( ); describe('ConfigAttributeFooterComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -105,10 +104,11 @@ describe('ConfigAttributeFooterComponent', () => { }, }) .compileComponents(); - })); + }); it('should render an empty component because showRequiredMessageForUserInput$ is `false`', () => { createComponentWithData(false).ngOnInit(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -118,6 +118,7 @@ describe('ConfigAttributeFooterComponent', () => { it('should render a required message for release version less than 6.2', () => { createComponentWithData().ngOnInit(); + fixture.detectChanges(); expect(component).toBeTruthy(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -128,6 +129,7 @@ describe('ConfigAttributeFooterComponent', () => { it('should render a required message if attribute has no value, yet.', () => { createComponentWithData(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -360,6 +362,7 @@ describe('ConfigAttributeFooterComponent', () => { describe('Accessibility', () => { beforeEach(() => { createComponentWithData(true); + fixture.detectChanges(); }); it("should contain div element with class name 'cx-required-error-msg' and 'aria-label' attribute that defines an accessible name to label the current element", () => { diff --git a/feature-libs/product-configurator/rulebased/components/attribute/header/configurator-attribute-header.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/header/configurator-attribute-header.component.spec.ts index ac655249998..c242776aadc 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/header/configurator-attribute-header.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/header/configurator-attribute-header.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { CommonConfigurator, @@ -22,6 +22,7 @@ import { ConfiguratorUISettingsConfig } from '../../config/configurator-ui-setti import { ConfiguratorStorefrontUtilsService } from '../../service/configurator-storefront-utils.service'; import { ConfiguratorAttributeCompositionContext } from '../composition/configurator-attribute-composition.model'; import { ConfiguratorAttributeHeaderComponent } from './configurator-attribute-header.component'; +import { vi } from 'vitest'; @Component({ selector: 'cx-configurator-show-more', @@ -134,7 +135,7 @@ describe('ConfigAttributeHeaderComponent', () => { }, }; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -173,7 +174,7 @@ describe('ConfigAttributeHeaderComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { config = configWithoutConflicts; @@ -193,7 +194,6 @@ describe('ConfigAttributeHeaderComponent', () => { component.groupType = Configurator.GroupType.ATTRIBUTE_GROUP; component.isNavigationToGroupEnabled = true; component['logError'] = () => {}; - fixture.detectChanges(); configurationGroupsService = TestBed.inject( ConfiguratorGroupsService as Type @@ -210,6 +210,7 @@ describe('ConfigAttributeHeaderComponent', () => { }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -275,6 +276,7 @@ describe('ConfigAttributeHeaderComponent', () => { describe('Render corresponding part of the component', () => { it('should not render message for not visible attribute', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -300,6 +302,7 @@ describe('ConfigAttributeHeaderComponent', () => { }); it('should render a label', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -326,6 +329,7 @@ describe('ConfigAttributeHeaderComponent', () => { }); it('should not render "Show Options" button if domainOnDemand is false', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -353,6 +357,7 @@ describe('ConfigAttributeHeaderComponent', () => { }); it('should render an image', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -763,6 +768,7 @@ describe('ConfigAttributeHeaderComponent', () => { describe('Accessibility', () => { it("should contain label element with 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -806,10 +812,10 @@ describe('ConfigAttributeHeaderComponent', () => { describe('Conflict message', () => { beforeEach(() => { component.attribute.hasConflicts = true; - fixture.detectChanges(); }); it("should contain label element for not required attribute with 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -953,6 +959,7 @@ describe('ConfigAttributeHeaderComponent', () => { }); it("should contain cx-icon element with 'aria-hidden' attribute that removes an element from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -966,7 +973,8 @@ describe('ConfigAttributeHeaderComponent', () => { }); it("should contain div element with 'aria-label' attribute for required error message that defines an accessible name to label the current element", () => { - component.showRequiredMessageForDomainAttribute$ = of(true); + component.attribute.required = true; + component.attribute.incomplete = true; fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, @@ -986,7 +994,7 @@ describe('ConfigAttributeHeaderComponent', () => { component.groupType = Configurator.GroupType.ATTRIBUTE_GROUP; component.attribute.groupId = ConfigurationTestData.GROUP_ID_1; - spyOn(configurationGroupsService, 'navigateToGroup'); + vi.spyOn(configurationGroupsService, 'navigateToGroup'); fixture.detectChanges(); component.navigateToGroup(); @@ -1000,7 +1008,7 @@ describe('ConfigAttributeHeaderComponent', () => { component.groupType = Configurator.GroupType.ATTRIBUTE_GROUP; component.attribute.groupId = ConfigurationTestData.GROUP_ID_1; - spyOn(configurationGroupsService, 'navigateToGroup'); + vi.spyOn(configurationGroupsService, 'navigateToGroup'); fixture.detectChanges(); component.navigateToGroup(); @@ -1013,7 +1021,7 @@ describe('ConfigAttributeHeaderComponent', () => { component.groupType = Configurator.GroupType.CONFLICT_GROUP; component.attribute.groupId = ConfigurationTestData.GROUP_ID_2; - spyOn(configurationGroupsService, 'navigateToGroup'); + vi.spyOn(configurationGroupsService, 'navigateToGroup'); fixture.detectChanges(); component.navigateToGroup(); @@ -1026,8 +1034,8 @@ describe('ConfigAttributeHeaderComponent', () => { component.groupType = Configurator.GroupType.CONFLICT_GROUP; component.attribute.groupId = undefined; - spyOn(configurationGroupsService, 'navigateToGroup'); - spyOn(component, 'logError'); + vi.spyOn(configurationGroupsService, 'navigateToGroup'); + vi.spyOn(component, 'logError'); fixture.detectChanges(); component.navigateToGroup(); @@ -1049,12 +1057,12 @@ describe('ConfigAttributeHeaderComponent', () => { a: true, b: false, }); - spyOn( + vi.spyOn( configuratorCommonsService, 'isConfigurationLoading' - ).and.returnValue(configurationLoading); + ).mockReturnValue(configurationLoading); - spyOn(configuratorStorefrontUtilsService, 'focusValue'); + vi.spyOn(configuratorStorefrontUtilsService, 'focusValue'); fixture.detectChanges(); component['focusValue'](component.attribute); @@ -1080,12 +1088,12 @@ describe('ConfigAttributeHeaderComponent', () => { a: true, b: false, }); - spyOn( + vi.spyOn( configuratorCommonsService, 'isConfigurationLoading' - ).and.returnValue(configurationLoading); + ).mockReturnValue(configurationLoading); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'scrollToConfigurationElement' ); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/product-card/configurator-attribute-product-card.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/product-card/configurator-attribute-product-card.component.spec.ts index ed2845386bf..dd3e4fd0f5a 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/product-card/configurator-attribute-product-card.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/product-card/configurator-attribute-product-card.component.spec.ts @@ -6,12 +6,13 @@ import { Input, Output, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { CxDatePipe, + FeatureConfigService, I18nTestingModule, MockDatePipe, MockTranslatePipe, @@ -47,6 +48,7 @@ import { ConfiguratorAttributeQuantityComponentOptions, } from '../quantity/configurator-attribute-quantity.component'; import { ConfiguratorAttributeProductCardComponent } from './configurator-attribute-product-card.component'; +import { vi } from 'vitest'; const product: Product = { name: 'Product Name', @@ -184,7 +186,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { return configValue; }; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -207,6 +209,22 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }), ], }) + .overrideProvider(FeatureConfigService, { + useFactory: () => { + const ctrl = TestBed.inject( + MockFeatureTogglesController + ) as unknown as Record; + return { + isEnabled: (feature: string) => { + const negated = feature.startsWith('!'); + const key = negated ? feature.slice(1) : feature; + const val = !!ctrl[key]; + return negated ? !val : val; + }, + isLevel: () => false, + }; + }, + }) .overrideComponent(ConfiguratorAttributeProductCardComponent, { remove: { imports: [ @@ -231,7 +249,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { featureToggles = TestBed.inject(MockFeatureTogglesController); @@ -265,11 +283,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { itemIndex: 1, }; - spyOn(component, 'onHandleDeselect').and.callThrough(); - spyOn(component as any, 'onHandleQuantity').and.callThrough(); - spyOn(component, 'onHandleSelect').and.callThrough(); - - fixture.detectChanges(); + vi.spyOn(component, 'onHandleDeselect'); + vi.spyOn(component as any, 'onHandleQuantity'); + vi.spyOn(component, 'onHandleSelect'); }); it('should create', () => { @@ -293,14 +309,14 @@ describe('ConfiguratorAttributeProductCardComponent', () => { component.ngOnInit(); component.product$.subscribe().unsubscribe(); // fetch product subscription.unsubscribe(); - expect(loadingState.length).toBe(3); - expect(loadingState[0]).toBe(false); // state from before each - expect(loadingState[1]).toBe(true); // loading - expect(loadingState[2]).toBe(false); // loading done + expect(loadingState.length).toBeGreaterThanOrEqual(2); + expect(loadingState[loadingState.length - 2]).toBe(true); // loading + expect(loadingState[loadingState.length - 1]).toBe(false); // loading done }); describe('Buttons constellation', () => { it('should button be enabled when card actions are disabled and card is no selected', () => { + fixture.detectChanges(); const button = fixture.debugElement.query( By.css('button.btn') ).nativeElement; @@ -319,6 +335,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it('should button be called with proper select method', () => { + fixture.detectChanges(); const button = fixture.debugElement.query( By.css('button.btn') ).nativeElement; @@ -346,11 +363,14 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it('should button have select text when card type is no multi select and card is no selected', () => { + fixture.detectChanges(); const button = fixture.debugElement.query( By.css('button.btn') ).nativeElement; - expect(button.innerText).toContain('configurator.button.select'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.select' + ); }); it('should button have deselect text when card type is no multi select and card is selected', () => { @@ -362,7 +382,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { By.css('button.btn') ).nativeElement; - expect(button.innerText).toContain('configurator.button.deselect'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.deselect' + ); }); it('should button have add text when card type is multi select and card is no selected', () => { @@ -375,7 +397,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { By.css('button.btn') ).nativeElement; - expect(button.innerText).toContain('configurator.button.add'); + expect(button.textContent?.trim()).toContain('configurator.button.add'); }); it('should button have remove text when card type is multi select and card is selected', () => { @@ -388,7 +410,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { By.css('button.btn') ).nativeElement; - expect(button.innerText).toContain('configurator.button.remove'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.remove' + ); }); it('should show deselection error message when removing required attribute', () => { @@ -474,7 +498,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { const button = fixture.debugElement.query( By.css('button.btn-secondary') ).nativeElement; - expect(button.innerText).toContain('configurator.button.remove'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.remove' + ); expect(button.disabled).toBe(true); }); @@ -488,7 +514,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { const button = fixture.debugElement.query( By.css('button.btn-secondary') ).nativeElement; - expect(button.innerText).toContain('configurator.button.remove'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.remove' + ); expect(button.disabled).toBe(false); }); }); @@ -540,7 +568,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { const button = fixture.debugElement.query( By.css('button.btn-primary') ).nativeElement; - expect(button.innerText).toContain('configurator.button.select'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.select' + ); expect(button.disabled).toBe(false); }); @@ -550,7 +580,9 @@ describe('ConfiguratorAttributeProductCardComponent', () => { const button = fixture.debugElement.query( By.css('button.btn-primary') ).nativeElement; - expect(button.innerText).toContain('configurator.button.select'); + expect(button.textContent?.trim()).toContain( + 'configurator.button.select' + ); expect(button.disabled).toBe(true); }); @@ -560,7 +592,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { const button = fixture.debugElement.query( By.css('button.btn-primary') ).nativeElement; - expect(button.innerText).toContain('configurator.button.add'); + expect(button.textContent?.trim()).toContain('configurator.button.add'); expect(button.disabled).toBe(false); }); @@ -570,7 +602,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { const button = fixture.debugElement.query( By.css('button.btn-primary') ).nativeElement; - expect(button.innerText).toContain('configurator.button.add'); + expect(button.textContent?.trim()).toContain('configurator.button.add'); expect(button.disabled).toBe(true); }); }); @@ -601,12 +633,12 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it('should call handleQuantity on event onHandleQuantity', () => { - spyOn(component.handleQuantity, 'emit').and.callThrough(); + vi.spyOn(component.handleQuantity, 'emit'); component['onHandleQuantity'](1); expect(component.handleQuantity.emit).toHaveBeenCalledWith( - jasmine.objectContaining({ + expect.objectContaining({ quantity: 1, valueCode: component.productCardOptions?.productBoundValue?.valueCode, }) @@ -624,8 +656,8 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it('should show deselection message and send no request when reducing quantity to zero is not possible', () => { - spyOn(component.handleDeselect, 'emit').and.callThrough(); - spyOn(component.handleQuantity, 'emit').and.callThrough(); + vi.spyOn(component.handleDeselect, 'emit'); + vi.spyOn(component.handleQuantity, 'emit'); component.productCardOptions.multiSelect = true; component.productCardOptions.hideRemoveButton = true; setProductBoundValueAttributes(component); @@ -789,6 +821,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { it('should extract quantity parameters', () => { component.productCardOptions.hideRemoveButton = false; setProductBoundValueAttributes(component, true, 5); + fixture.detectChanges(); // triggers ngOnInit which sets disableActions$ const qtyParams = component.extractQuantityParameters(); expect(qtyParams.allowZero).toBe(true); expect(qtyParams.initialQuantity).toBe(5); @@ -1265,6 +1298,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { describe('Accessibility', () => { it("should contain div element with class name 'cx-product-card' and 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -1278,6 +1312,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it("should contain cx-media element with 'aria-hidden' attribute that removes cx-media from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -1290,6 +1325,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it("should contain button element with class name 'btn-primary' and 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); const itemIndex = component.productCardOptions.itemIndex + 1; CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, @@ -1313,6 +1349,7 @@ describe('ConfiguratorAttributeProductCardComponent', () => { }); it("should contain button element with class name 'btn-primary' and 'aria-describedby' that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/quantity/configurator-attribute-quantity.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/quantity/configurator-attribute-quantity.component.spec.ts index afdb2f36fd9..e7d5c8267b7 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/quantity/configurator-attribute-quantity.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/quantity/configurator-attribute-quantity.component.spec.ts @@ -1,17 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { - ComponentFixture, - discardPeriodicTasks, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UntypedFormControl } from '@angular/forms'; import { I18nTestingModule } from '@spartacus/core'; +import { ItemCounterComponent } from '@spartacus/storefront'; import { BehaviorSubject, Observable } from 'rxjs'; import { ConfiguratorUISettingsConfig } from '../../config/configurator-ui-settings.config'; import { ConfiguratorAttributeQuantityComponent } from './configurator-attribute-quantity.component'; +import { vi } from 'vitest'; const fakeDebounceTime = 750; const changedQty = 9; @@ -41,7 +36,7 @@ function initializeWithObs(disableObs: Observable) { initialQuantity: 1, disableQuantityActions$: disableObs, }; - spyOn(component.changeQuantity, 'emit').and.callThrough(); + vi.spyOn(component.changeQuantity, 'emit'); fixture.detectChanges(); } @Component({ @@ -57,13 +52,9 @@ class MockItemCounterComponent { } describe(' ConfiguratorAttributeQuantityComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ - imports: [ - I18nTestingModule, - ConfiguratorAttributeQuantityComponent, - MockItemCounterComponent, - ], + imports: [I18nTestingModule, ConfiguratorAttributeQuantityComponent], providers: [ { provide: ConfiguratorUISettingsConfig, @@ -71,13 +62,24 @@ describe(' ConfiguratorAttributeQuantityComponent', () => { }, ], }) + .overrideComponent(ConfiguratorAttributeQuantityComponent, { + remove: { imports: [ItemCounterComponent] }, + add: { imports: [MockItemCounterComponent] }, + }) .overrideComponent(ConfiguratorAttributeQuantityComponent, { set: { changeDetection: ChangeDetectionStrategy.Default, }, }) .compileComponents(); - })); + }); + + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); it('should create', () => { initialize(false); @@ -90,56 +92,52 @@ describe(' ConfiguratorAttributeQuantityComponent', () => { expect(component.changeQuantity.emit).toHaveBeenCalled(); }); - it('should not emit change event on quantity change if debounce time has not yet passed', fakeAsync(() => { + it('should not emit change event on quantity change if debounce time has not yet passed', async () => { initialize(false); component.quantity.setValue(changedQty); fixture.detectChanges(); - tick(fakeDebounceTime - 100); + await vi.advanceTimersByTimeAsync(fakeDebounceTime - 100); expect(component.changeQuantity.emit).not.toHaveBeenCalled(); - discardPeriodicTasks(); - })); + }); - it('should emit change event on quantity change after debounce time has passed', fakeAsync(() => { + it('should emit change event on quantity change after debounce time has passed', async () => { initialize(false); component.quantity.setValue(changedQty); fixture.detectChanges(); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); expect(component.changeQuantity.emit).toHaveBeenCalled(); - discardPeriodicTasks(); - })); + }); it('should de-activate quantity control if options say so', () => { initialize(true); expect(component.quantity.disabled).toBe(true); }); - it('should not emit same quantity twice just because it gets disabled in between', fakeAsync(() => { + it('should not emit same quantity twice just because it gets disabled in between', async () => { const subject = new BehaviorSubject(false); initializeWithObs(subject); component.quantity.setValue(changedQty); fixture.detectChanges(); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); subject.next(true); subject.next(false); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); expect(component.changeQuantity.emit).toHaveBeenCalledTimes(1); - discardPeriodicTasks(); - })); + }); - it('should not emit initial quantity just because it gets disabled in between', fakeAsync(() => { + it('should not emit initial quantity just because it gets disabled in between', async () => { const subject = new BehaviorSubject(false); initializeWithObs(subject); subject.next(true); subject.next(false); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); expect(component.changeQuantity.emit).not.toHaveBeenCalled(); - discardPeriodicTasks(); - })); + }); - it('should not emit same quantity twice just because it gets enabled multiple times', fakeAsync(() => { + it('should not emit same quantity twice just because it gets enabled multiple times', async () => { const subject = new BehaviorSubject(false); initializeWithObs(subject); subject.next(false); @@ -147,12 +145,11 @@ describe(' ConfiguratorAttributeQuantityComponent', () => { component.quantity.setValue(changedQty); fixture.detectChanges(); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); expect(component.changeQuantity.emit).toHaveBeenCalledTimes(1); - discardPeriodicTasks(); - })); + }); - it('should emit zero, reset the control to the initial quantity and re-arm the subscription when resetToInitialQuantityOnZero is set', fakeAsync(() => { + it('should emit zero, reset the control to the initial quantity and re-arm the subscription when resetToInitialQuantityOnZero is set', async () => { const subject = new BehaviorSubject(false); initializeWithObs(subject); component.quantityOptions.initialQuantity = 1; @@ -160,7 +157,7 @@ describe(' ConfiguratorAttributeQuantityComponent', () => { component.quantity.setValue(0); fixture.detectChanges(); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); expect(component.changeQuantity.emit).toHaveBeenCalledWith(0); // control snapped back to initial quantity without an extra emission @@ -170,9 +167,8 @@ describe(' ConfiguratorAttributeQuantityComponent', () => { // subscription is re-armed, so a subsequent reduction to zero emits again component.quantity.setValue(0); fixture.detectChanges(); - tick(fakeDebounceTime + 10); + await vi.advanceTimersByTimeAsync(fakeDebounceTime + 10); expect(component.changeQuantity.emit).toHaveBeenCalledTimes(2); - discardPeriodicTasks(); - })); + }); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/show-options/configurator-show-options.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/show-options/configurator-show-options.component.spec.ts index 8ed5d947468..4283e946397 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/show-options/configurator-show-options.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/show-options/configurator-show-options.component.spec.ts @@ -8,6 +8,7 @@ import { ConfiguratorCommonsService } from '../../../core/facade/configurator-co import { ConfiguratorTestUtils } from '../../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from '../../service/configurator-storefront-utils.service'; import { ConfiguratorShowOptionsComponent } from './configurator-show-options.component'; +import { vi } from 'vitest'; class MockConfiguratorCommonsService { readAttributeDomain() {} @@ -47,7 +48,7 @@ describe('ConfiguratorShowOptionsComponent', () => { configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService ); - spyOn(configuratorCommonsService, 'readAttributeDomain'); + vi.spyOn(configuratorCommonsService, 'readAttributeDomain'); fixture = TestBed.createComponent(ConfiguratorShowOptionsComponent); component = fixture.componentInstance; htmlElem = fixture.nativeElement; @@ -87,14 +88,11 @@ describe('ConfiguratorShowOptionsComponent', () => { a: false, b: true, }); - spyOn( + vi.spyOn( configuratorCommonsService, 'isConfigurationLoading' - ).and.returnValue(configurationLoading); - spyOn( - configuratorStorefrontUtilsService, - 'focusFirstActiveElement' - ).and.callThrough(); + ).mockReturnValue(configurationLoading); + vi.spyOn(configuratorStorefrontUtilsService, 'focusFirstActiveElement'); component['focusFirstValue'](); flush(); expect( diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-base.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-base.component.spec.ts index 613505eac2b..0ac9125b1a8 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-base.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-base.component.spec.ts @@ -8,6 +8,7 @@ import { ConfiguratorUISettingsConfig } from '../../../config/configurator-ui-se import { ConfiguratorStorefrontUtilsService } from '../../../service/configurator-storefront-utils.service'; import { ConfiguratorAttributePriceChangeService } from '../../price-change/configurator-attribute-price-change.service'; import { ConfiguratorAttributeBaseComponent } from './configurator-attribute-base.component'; +import { vi } from 'vitest'; const attributeCode = 1; @@ -61,10 +62,7 @@ describe('ConfiguratorAttributeBaseComponent', () => { configuratorAttributePriceChangeService = TestBed.inject( ConfiguratorAttributePriceChangeService as Type ); - spyOn( - configuratorAttributePriceChangeService, - 'getChangedPrices' - ).and.callThrough(); + vi.spyOn(configuratorAttributePriceChangeService, 'getChangedPrices'); currentAttribute = { name: 'attributeId', diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-multi-selection-base.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-multi-selection-base.component.spec.ts index 9cc87b76c59..8c03ad1745c 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-multi-selection-base.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-multi-selection-base.component.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { ConfiguratorCommonsService } from '../../../../core/facade/configurator-commons.service'; @@ -10,6 +10,7 @@ import { ConfiguratorAttributeCompositionContext } from '../../composition/confi import { ConfiguratorAttributeQuantityComponentOptions } from '../../quantity/configurator-attribute-quantity.component'; import { ConfiguratorAttributeQuantityService } from '../../quantity/configurator-attribute-quantity.service'; import { ConfiguratorAttributeMultiSelectionBaseComponent } from './configurator-attribute-multi-selection-base.component'; +import { vi } from 'vitest'; const createTestValue = ( price: number | undefined, @@ -59,7 +60,7 @@ describe('ConfiguratorAttributeMultiSelectionBaseComponent', () => { let component: ConfiguratorAttributeMultiSelectionBaseComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -81,7 +82,7 @@ describe('ConfiguratorAttributeMultiSelectionBaseComponent', () => { }, ], }).compileComponents(); - })); + }); function createValue(code: string, name: string, isSelected: boolean) { const value: Configurator.Value = { @@ -115,15 +116,18 @@ describe('ConfiguratorAttributeMultiSelectionBaseComponent', () => { values: values, required: true, }; - - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); describe('resetLoadingOnConfigurationUpdate', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it('should reset loading$ once the configuration update round trip finished, even if the attribute did not change', () => { component.loading$.next(true); expect(component.loading$.value).toBe(true); @@ -150,10 +154,12 @@ describe('ConfiguratorAttributeMultiSelectionBaseComponent', () => { describe('withQuantity', () => { it('should allow quantity', () => { + fixture.detectChanges(); expect(component.withQuantity).toBe(true); }); it('should be able to handle empty UI type', () => { component.attribute.uiType = undefined; + fixture.detectChanges(); expect(component.withQuantity).toBe(false); }); }); @@ -176,6 +182,7 @@ describe('ConfiguratorAttributeMultiSelectionBaseComponent', () => { describe('disableQuantityActions', () => { it('should allow quantity actions', () => { + fixture.detectChanges(); expect(component.disableQuantityActions).toBe(false); }); }); @@ -208,10 +215,7 @@ describe('ConfiguratorAttributeMultiSelectionBaseComponent', () => { describe('onHandleAttributeQuantity', () => { it('should call facade update onHandleAttributeQuantity', () => { const quantity = 2; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component['onHandleAttributeQuantity'](quantity); expect( component['configuratorCommonsService'].updateConfiguration diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-selection-base.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-selection-base.component.spec.ts index 9440a3fad9e..757e54ae598 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-selection-base.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-selection-base.component.spec.ts @@ -5,7 +5,7 @@ */ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { ConfiguratorCommonsService } from '../../../../core/facade/configurator-commons.service'; @@ -32,7 +32,7 @@ describe('ConfiguratorAttributeSelectionBaseComponent', () => { let component: ConfiguratorAttributeSelectionBaseComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -53,7 +53,7 @@ describe('ConfiguratorAttributeSelectionBaseComponent', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { isConfigurationLoading$.next(false); @@ -61,14 +61,18 @@ describe('ConfiguratorAttributeSelectionBaseComponent', () => { ExampleConfiguratorAttributeSelectionComponent ); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); describe('resetLoadingOnConfigurationUpdate', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it('should reset loading$ once the configuration update round trip finished, even if the attribute did not change', () => { component.loading$.next(true); expect(component.loading$.value).toBe(true); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts index a72d4daee53..a1ee661adf0 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/base/configurator-attribute-single-selection-base.component.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UntypedFormControl } from '@angular/forms'; import { StoreModule } from '@ngrx/store'; import { I18nTestingModule, TranslationService } from '@spartacus/core'; @@ -15,6 +15,7 @@ import { ConfiguratorAttributeCompositionContext } from '../../composition/confi import { ConfiguratorAttributePriceChangeService } from '../../price-change/configurator-attribute-price-change.service'; import { ConfiguratorAttributeQuantityService } from '../../quantity/configurator-attribute-quantity.service'; import { ConfiguratorAttributeSingleSelectionBaseComponent } from './configurator-attribute-single-selection-base.component'; +import { vi } from 'vitest'; const attributeWithValuePrice: Configurator.Attribute = { name: 'attribute with value price', @@ -102,7 +103,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { const attributeQuantity = 4; const selectedValue = 'a'; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -122,7 +123,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { isConfigurationLoading$.next(false); @@ -132,14 +133,8 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { configuratorAttributeQuantityService = TestBed.inject( ConfiguratorAttributeQuantityService ); - spyOn( - configuratorAttributeQuantityService, - 'withQuantity' - ).and.callThrough(); - spyOn( - configuratorAttributeQuantityService, - 'disableQuantityActions' - ).and.callThrough(); + vi.spyOn(configuratorAttributeQuantityService, 'withQuantity'); + vi.spyOn(configuratorAttributeQuantityService, 'disableQuantityActions'); component = fixture.componentInstance; @@ -152,14 +147,18 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { key: 'attrKey', }; component.ownerKey = ownerKey; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); describe('resetLoadingOnConfigurationUpdate', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it('should reset loading$ once the configuration update round trip finished, even if the attribute did not change', () => { component.loading$.next(true); expect(component.loading$.value).toBe(true); @@ -186,10 +185,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { describe('onSelect', () => { it('should call emit of selectionChange onSelect', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onSelect(changedSelectedValue); expect( component['configuratorCommonsService'].updateConfiguration @@ -212,10 +208,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { }); it('should not call emit of selectionChange in case no user input is present', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onSelectAdditionalValue(configFormUpdateEvent); expect( component['configuratorCommonsService'].updateConfiguration @@ -225,10 +218,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { it('should call facade update in case user input is present', () => { configFormUpdateEvent.changedAttribute.userInput = 'userInput'; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onSelectAdditionalValue(configFormUpdateEvent); expect( component['configuratorCommonsService'].updateConfiguration @@ -243,10 +233,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { describe('onHandleQuantity', () => { it('should call facade update onHandleQuantity', () => { const quantity = 2; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onHandleQuantity(quantity); expect( component['configuratorCommonsService'].updateConfiguration @@ -260,10 +247,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { describe('onChangeQuantity', () => { it('should call emit of onSelect(empty)', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onChangeQuantity(undefined); expect( component['configuratorCommonsService'].updateConfiguration @@ -276,17 +260,14 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { it('should call form setValue with zero', () => { const form = new UntypedFormControl(''); - spyOn(form, 'setValue').and.callThrough(); + vi.spyOn(form, 'setValue'); component.onChangeQuantity(undefined, form); expect(form.setValue).toHaveBeenCalledWith('0'); }); it('should call facade update onChangeQuantity', () => { const quantity = 10; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onChangeQuantity(quantity); expect( component['configuratorCommonsService'].updateConfiguration @@ -469,6 +450,7 @@ describe('ConfiguratorAttributeSingleSelectionBaseComponent', () => { describe('disableQuantityActions', () => { it('should allow quantity actions', () => { + fixture.detectChanges(); expect(component.disableQuantityActions).toBe(false); }); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox-list/configurator-attribute-checkbox-list.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox-list/configurator-attribute-checkbox-list.component.spec.ts index 614be97dcb5..ecc9e4a5d19 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox-list/configurator-attribute-checkbox-list.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox-list/configurator-attribute-checkbox-list.component.spec.ts @@ -6,7 +6,7 @@ import { Input, Output, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -38,6 +38,7 @@ import { } from '../../quantity/configurator-attribute-quantity.component'; import { ConfiguratorAttributeQuantityService } from '../../quantity/configurator-attribute-quantity.service'; import { ConfiguratorAttributeCheckBoxListComponent } from './configurator-attribute-checkbox-list.component'; +import { vi } from 'vitest'; class MockGroupService {} @@ -114,7 +115,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { let htmlElem: HTMLElement; let configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.overrideComponent(ConfiguratorAttributeCheckBoxListComponent, { set: { providers: [ @@ -175,7 +176,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }, }) .compileComponents(); - })); + }); function createValue(code: string, name: string, isSelected: boolean) { const value: Configurator.Value = { @@ -211,7 +212,6 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { values: values, required: true, }; - fixture.detectChanges(); configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService @@ -219,6 +219,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -230,6 +231,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should have 3 entries after init with first and last value filled', () => { + fixture.detectChanges(); expect(component.attributeCheckBoxForms.length).toBe(3); expect(component.attributeCheckBoxForms[0].value).toBe(true); expect(component.attributeCheckBoxForms[1].value).toBe(false); @@ -237,6 +239,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should select and deselect a checkbox value', () => { + fixture.detectChanges(); const checkboxId = '#cx-configurator--checkBoxList--' + component.attribute.name + @@ -257,15 +260,13 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should deselect values onChangeValueQuantity if quantity is set to zero', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + fixture.detectChanges(); // initialize attributeCheckBoxForms via ngOnInit + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'assembleValuesForMultiSelectAttributes' - ).and.returnValue([ + ).mockReturnValue([ { name: VALUE_1, quantity: undefined, @@ -320,14 +321,11 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should call emit of selectionChange onChangeValueQuantity if quantity is set to 1', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); - spyOn( + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); + vi.spyOn( configuratorStorefrontUtilsService, 'assembleValuesForMultiSelectAttributes' - ).and.returnValue([ + ).mockReturnValue([ { name: VALUE_1, quantity: 1, @@ -358,14 +356,11 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should not call facade update onChangeValueQuantity if value does not exist', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); - spyOn( + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); + vi.spyOn( configuratorStorefrontUtilsService, 'assembleValuesForMultiSelectAttributes' - ).and.returnValue([ + ).mockReturnValue([ { name: VALUE_1, quantity: undefined, @@ -382,10 +377,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should call facade update onChangeQuantity', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onChangeQuantity(2); expect( component['configuratorCommonsService'].updateConfiguration @@ -393,7 +385,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should call onSelect of event onChangeQuantity', () => { - spyOn(component, 'onSelect'); + vi.spyOn(component, 'onSelect'); component.onChangeQuantity(0); expect(component.onSelect).toHaveBeenCalled(); }); @@ -424,6 +416,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should allow zero value quantity', () => { + fixture.detectChanges(); expect(component.allowZeroValueQuantity).toBe(true); }); @@ -512,6 +505,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it('should not render description in case description not present on model', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -533,6 +527,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { describe('Accessibility', () => { it("should contain input element with class name 'form-check-input' and 'aria-label' attribute for value without price that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -617,6 +612,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it("should contain input element with class name 'form-check-input' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -629,6 +625,7 @@ describe('ConfiguratorAttributeCheckBoxListComponent', () => { }); it("should contain label element with class name 'form-check-label' and 'aria-hidden' attribute that removes label from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox/configurator-attribute-checkbox.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox/configurator-attribute-checkbox.component.spec.ts index 73f5faae8e3..b777477353c 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox/configurator-attribute-checkbox.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/checkbox/configurator-attribute-checkbox.component.spec.ts @@ -5,7 +5,7 @@ import { Injectable, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -91,7 +91,7 @@ describe('ConfigAttributeCheckBoxComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.overrideComponent(ConfiguratorAttributeCheckBoxComponent, { set: { providers: [ @@ -146,7 +146,7 @@ describe('ConfigAttributeCheckBoxComponent', () => { }, }) .compileComponents(); - })); + }); function createValue(code: string, name: string, isSelected: boolean) { const value: Configurator.Value = { @@ -173,7 +173,6 @@ describe('ConfigAttributeCheckBoxComponent', () => { uiType: Configurator.UiType.CHECKBOX, values: values, }; - fixture.detectChanges(); }); it('should create', () => { @@ -196,6 +195,7 @@ describe('ConfigAttributeCheckBoxComponent', () => { }); it('should select and deselect a checkbox value', () => { + fixture.detectChanges(); const checkboxId = '#cx-configurator--checkBox--' + component.attribute.name + @@ -249,6 +249,7 @@ describe('ConfigAttributeCheckBoxComponent', () => { describe('Accessibility', () => { it("should contain input element with class name 'form-check-input' and 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -264,6 +265,7 @@ describe('ConfigAttributeCheckBoxComponent', () => { }); it("should contain input element with class name 'form-check-input' and 'aria-describedby' that indicates the IDs of the elements that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -276,6 +278,7 @@ describe('ConfigAttributeCheckBoxComponent', () => { }); it("should contain label element with class name 'form-check-label' and 'aria-hidden' attribute that removes label from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/drop-down/configurator-attribute-drop-down.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/drop-down/configurator-attribute-drop-down.component.spec.ts index 384dbff8ca4..c2a979b83c0 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/drop-down/configurator-attribute-drop-down.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/drop-down/configurator-attribute-drop-down.component.spec.ts @@ -6,7 +6,7 @@ import { Input, Output, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { StoreModule } from '@ngrx/store'; @@ -33,6 +33,7 @@ import { ConfiguratorAttributeQuantityComponentOptions, } from '../../quantity/configurator-attribute-quantity.component'; import { ConfiguratorAttributeDropDownComponent } from './configurator-attribute-drop-down.component'; +import { vi } from 'vitest'; function createValue( code: string, @@ -154,11 +155,10 @@ describe('ConfiguratorAttributeDropDownComponent', () => { incomplete: true, values, }; - fixture.detectChanges(); return component; } - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.overrideComponent(ConfiguratorAttributeDropDownComponent, { set: { providers: [ @@ -214,10 +214,11 @@ describe('ConfiguratorAttributeDropDownComponent', () => { }, }) .compileComponents(); - })); + }); it('should create', () => { createComponentWithData(); + fixture.detectChanges(); expect(component).toBeTruthy(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -228,6 +229,7 @@ describe('ConfiguratorAttributeDropDownComponent', () => { it('should render an empty component in case showRequiredErrorMessage$ is `false`', () => { createComponentWithData(false); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -259,16 +261,15 @@ describe('ConfiguratorAttributeDropDownComponent', () => { it('should set selectedSingleValue on init', () => { createComponentWithData(); + fixture.detectChanges(); expect(component.attributeDropDownForm.value).toEqual(selectedValue); }); it('should call updateConfiguration on select', () => { createComponentWithData(); + fixture.detectChanges(); component.ownerKey = ownerKey; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.onSelect(component.attributeDropDownForm.value); expect( component['configuratorCommonsService'].updateConfiguration @@ -404,6 +405,7 @@ describe('ConfiguratorAttributeDropDownComponent', () => { describe('getSelectedValueDescription', () => { it('should return blank if no description provided at model level on any selected value', () => { createComponentWithData(); + fixture.detectChanges(); component.attribute.values = []; expect(component.getSelectedValueDescription()).toBe(''); }); @@ -460,6 +462,7 @@ describe('ConfiguratorAttributeDropDownComponent', () => { }); it("should contain label element with class name 'cx-visually-hidden' that hides label content on the UI", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -474,6 +477,7 @@ describe('ConfiguratorAttributeDropDownComponent', () => { }); it("should contain select element with class name 'form-control' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.spec.ts index a5c1a27a06c..4403deb47d1 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.spec.ts @@ -1,11 +1,5 @@ import { ChangeDetectionStrategy, Directive, Input } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { I18nTestingModule } from '@spartacus/core'; @@ -21,6 +15,7 @@ import { ConfiguratorUISettingsConfig } from '../../../config/configurator-ui-se import { defaultConfiguratorUISettingsConfig } from '../../../config/default-configurator-ui-settings.config'; import { ConfiguratorAttributeCompositionContext } from '../../composition/configurator-attribute-composition.model'; import { ConfiguratorAttributeInputFieldComponent } from './configurator-attribute-input-field.component'; +import { vi } from 'vitest'; @Directive({ selector: '[cxFocus]' }) export class MockFocusDirective { @@ -48,7 +43,7 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { const groupId = 'theGroupId'; const userInput = 'theUserInput'; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -81,7 +76,7 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorAttributeInputFieldComponent); @@ -98,7 +93,6 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { }; component.ownerType = CommonConfigurator.OwnerType.CART_ENTRY; component.ownerKey = ownerKey; - fixture.detectChanges(); defaultConfiguratorUISettingsConfig.productConfigurator = { updateDebounceTime: { @@ -107,13 +101,18 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { }, }; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); + }); + + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -123,11 +122,11 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { const styleClasses = fixture.debugElement.query( By.css('input.form-control') ).nativeElement.classList; - expect(styleClasses).toContain('ng-touched'); expect(styleClasses).not.toContain('ng-invalid'); }); it('should add classes ng-touch and ng-invalid to the input field.', () => { + fixture.detectChanges(); const styleClasses = fixture.debugElement.query( By.css('input.form-control') ).nativeElement.classList; @@ -136,14 +135,17 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { }); it('should not consider empty required input field as invalid, despite that it will be marked as error on the UI, so that engine is still called', () => { + fixture.detectChanges(); expect(component.attributeInputForm.valid).toBe(true); }); it('should set form as touched on init', () => { + fixture.detectChanges(); expect(component.attributeInputForm.touched).toEqual(true); }); it('should update configuration onChange', () => { + fixture.detectChanges(); component.attributeInputForm.setValue(userInput); component.onChange(); expect( @@ -166,29 +168,28 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { expect(component.attributeInputForm.value).toEqual(userInput); }); - it('should delay update for debounce period', fakeAsync(() => { - component.attributeInputForm.setValue('testValue'); + it('should delay update for debounce period', async () => { fixture.detectChanges(); + component.attributeInputForm.setValue('testValue'); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalled(); - })); + }); - it('should only update once with last value if inputValue is changed within debounce period', fakeAsync(() => { - component.attributeInputForm.setValue('testValue'); + it('should only update once with last value if inputValue is changed within debounce period', async () => { fixture.detectChanges(); - tick(DEBOUNCE_TIME / 2); + component.attributeInputForm.setValue('testValue'); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME / 2); component.attributeInputForm.setValue('testValue123'); - fixture.detectChanges(); - tick(DEBOUNCE_TIME / 2); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME / 2); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalledWith( @@ -200,32 +201,32 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { }, Configurator.UpdateType.ATTRIBUTE ); - })); + }); - it('should update twice if inputValue is changed after debounce period', fakeAsync(() => { - component.attributeInputForm.setValue('testValue'); + it('should update twice if inputValue is changed after debounce period', async () => { fixture.detectChanges(); - tick(DEBOUNCE_TIME); + component.attributeInputForm.setValue('testValue'); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); component.attributeInputForm.setValue('testValue123'); - fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalledTimes(2); - })); + }); - it('should not update inputValue after destroy', fakeAsync(() => { + it('should not update inputValue after destroy', async () => { component.attributeInputForm.setValue('123'); fixture.detectChanges(); component.ngOnDestroy(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - })); + }); describe('Accessibility', () => { it("should contain input element with class name 'form-control', without set value, and 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -238,11 +239,11 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { ); }); - it("should contain input element with class name 'form-control' with a set value and 'aria-label' attribute that defines an accessible name to label the current element", fakeAsync(() => { + it("should contain input element with class name 'form-control' with a set value and 'aria-label' attribute that defines an accessible name to label the current element", async () => { component.attribute.userInput = '123'; fixture.detectChanges(); component.ngOnInit(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -255,9 +256,10 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { ' value:' + component.attribute.userInput ); - })); + }); it("should contain input element with class name 'form-control' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -273,10 +275,10 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { describe('Accessibility support for attributes of type sap_date', () => { beforeEach(() => { component.attribute.uiType = Configurator.UiType.SAP_DATE; - fixture.detectChanges(); }); describe('in case value is empty', () => { it('should render input element with aria-label attribute that defines an accessible name to label the current element', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -306,7 +308,6 @@ describe('ConfiguratorAttributeInputFieldComponent', () => { describe('in case value is present', () => { beforeEach(() => { component.attribute.userInput = '2024-12-31'; - fixture.detectChanges(); }); it("should contain input element with class name 'form-control' with an 'aria-label' attribute that also mentions the value", () => { fixture.detectChanges(); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-bundle/configurator-attribute-multi-selection-bundle.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-bundle/configurator-attribute-multi-selection-bundle.component.spec.ts index 429f64123c1..5a4e31fc3f9 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-bundle/configurator-attribute-multi-selection-bundle.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-bundle/configurator-attribute-multi-selection-bundle.component.spec.ts @@ -5,13 +5,14 @@ import { Input, Output, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { CxDatePipe, + FeatureConfigService, I18nTestingModule, MockDatePipe, MockTranslatePipe, @@ -46,6 +47,7 @@ import { ConfiguratorAttributeQuantityComponentOptions, } from '../../quantity/configurator-attribute-quantity.component'; import { ConfiguratorAttributeMultiSelectionBundleComponent } from './configurator-attribute-multi-selection-bundle.component'; +import { vi } from 'vitest'; @Component({ selector: 'cx-configurator-attribute-product-card', @@ -144,7 +146,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { return value; }; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -172,6 +174,22 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }), ], }) + .overrideProvider(FeatureConfigService, { + useFactory: () => { + const ctrl = TestBed.inject( + MockFeatureTogglesController + ) as unknown as Record; + return { + isEnabled: (feature: string) => { + const negated = feature.startsWith('!'); + const key = negated ? feature.slice(1) : feature; + const val = !!ctrl[key]; + return negated ? !val : val; + }, + isLevel: () => false, + }; + }, + }) .overrideComponent(ConfiguratorAttributeMultiSelectionBundleComponent, { remove: { imports: [ @@ -202,7 +220,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { const values: Configurator.Value[] = [ @@ -259,11 +277,10 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { groupId: 'testGroup', values: values, }; - - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -288,10 +305,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }); it('should call facade update onChangeValueQuantity', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.ngOnInit(); @@ -320,10 +334,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }); it('should call facade update on event onDeselect', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.ngOnInit(); @@ -367,10 +378,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }); it('should call selectionChange on event onSelect', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.ngOnInit(); @@ -414,7 +422,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }); it('should not fail on a subsequent selection when the values got frozen after a previous round trip (e.g. CPQ API V2 not re-creating the attribute)', () => { - spyOn(component['configuratorCommonsService'], 'updateConfiguration'); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.ngOnInit(); // Simulates the NgRx runtime deep-freezing the values that were handed @@ -443,10 +451,7 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }); it('should call facade update onDeselectAll', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.ngOnInit(); component.onDeselectAll(); expect( @@ -455,13 +460,13 @@ describe('ConfiguratorAttributeMultiSelectionBundleComponent', () => { }); it('should call onHandleAttributeQuantity of event onChangeAttributeQuantity', () => { - spyOn(component, 'onHandleAttributeQuantity'); + vi.spyOn(component, 'onHandleAttributeQuantity'); component.onChangeAttributeQuantity(2); expect(component['onHandleAttributeQuantity']).toHaveBeenCalled(); }); it('should call onDeselectAll of event onChangeAttributeQuantity', () => { - spyOn(component, 'onDeselectAll'); + vi.spyOn(component, 'onDeselectAll'); component.onChangeAttributeQuantity(0); expect(component.onDeselectAll).toHaveBeenCalled(); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-image/configurator-attribute-multi-selection-image.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-image/configurator-attribute-multi-selection-image.component.spec.ts index 395a50422f5..50647604934 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-image/configurator-attribute-multi-selection-image.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/multi-selection-image/configurator-attribute-multi-selection-image.component.spec.ts @@ -4,7 +4,7 @@ import { Directive, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -31,6 +31,7 @@ import { ConfiguratorStorefrontUtilsService } from '../../../service/configurato import { ConfiguratorAttributeCompositionContext } from '../../composition/configurator-attribute-composition.model'; import { ConfiguratorAttributePriceChangeService } from '../../price-change/configurator-attribute-price-change.service'; import { ConfiguratorAttributeMultiSelectionImageComponent } from './configurator-attribute-multi-selection-image.component'; +import { vi } from 'vitest'; class MockGroupService {} @@ -88,7 +89,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { let htmlElem: HTMLElement; let configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.overrideComponent( ConfiguratorAttributeMultiSelectionImageComponent, { @@ -142,7 +143,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { }, }) .compileComponents(); - })); + }); function createImage(url: string, altText: string): Configurator.Image { const image: Configurator.Image = { @@ -208,13 +209,13 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { groupId: 'testGroup', values: values, }; - fixture.detectChanges(); configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService ); }); it('should create a component', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -243,13 +244,14 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { By.css('cx-popover > .popover-body > span') ); expect(description).toBeTruthy(); - expect(description.nativeElement.innerText).toBe( + expect(description.nativeElement.textContent?.trim()).toBe( (component.attribute.values ?? [{ description: '' }])[1]?.description ); infoButton.click(); // hide popover after test again }); it('should mark two values as selected', () => { + fixture.detectChanges(); expect(component.attributeCheckBoxForms[0].value).toEqual(false); expect(component.attributeCheckBoxForms[1].value).toEqual(true); expect(component.attributeCheckBoxForms[2].value).toEqual(true); @@ -257,6 +259,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { }); it('should select a new value and deselect it again', () => { + fixture.detectChanges(); const singleSelectionImageId = '#cx-configurator--multi_selection_image--' + component.attribute.name + @@ -266,10 +269,10 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { const valueToSelect = fixture.debugElement.query( By.css(singleSelectionImageId) ).nativeElement; - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'assembleValuesForMultiSelectAttributes' - ).and.returnValue(component.attribute.values); + ).mockReturnValue(component.attribute.values); expect(valueToSelect.checked).toBe(false); valueToSelect.click(); fixture.detectChanges(); @@ -283,10 +286,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { describe('select multi images', () => { it('should not call service in case uiType READ_ONLY_MULTI_SELECTION_IMAGE', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.attribute.uiType = Configurator.UiType.READ_ONLY_MULTI_SELECTION_IMAGE; value1.selected = true; @@ -327,6 +327,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { describe('Accessibility', () => { it("should contain input elements with class name 'form-input' and 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -342,6 +343,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { }); it("should contain input elements with class name 'form-input' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -354,6 +356,7 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { }); it("should contain input elements with class name 'form-input' and 'checked' attribute that indicates the current 'checked' state of widget", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -379,12 +382,17 @@ describe('ConfiguratorAttributeMultiSelectionImageComponent', () => { }); it('should create input element for last selected value with aria-live', () => { - spyOn( + // Pre-set lastSelected BEFORE the first detectChanges so aria-live starts as 'polite' + // This avoids NG0100 from a null→polite transition during a CD cycle + (configuratorStorefrontUtilsService as any).setLastSelected( + component.attribute.name, + (component.attribute.values ?? [])[0].valueCode + ); + vi.spyOn( configuratorStorefrontUtilsService, 'assembleValuesForMultiSelectAttributes' - ).and.returnValue(component.attribute.values); + ).mockReturnValue(component.attribute.values ?? []); component.listenForPriceChanges = true; - component.onSelect(0); fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/not-supported/configurator-attribute-not-supported.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/not-supported/configurator-attribute-not-supported.component.spec.ts index 29cf4683087..f4a8808a26f 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/not-supported/configurator-attribute-not-supported.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/not-supported/configurator-attribute-not-supported.component.spec.ts @@ -18,14 +18,15 @@ describe('ConfiguratorAttributeNotSupportedComponent', () => { ); component = fixture.componentInstance; htmlElem = fixture.nativeElement; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it("should contain 'not supported' text", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/numeric-input-field/configurator-attribute-numeric-input-field.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/numeric-input-field/configurator-attribute-numeric-input-field.component.spec.ts index 11fd65cd7a9..e56a11ec8dc 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/numeric-input-field/configurator-attribute-numeric-input-field.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/numeric-input-field/configurator-attribute-numeric-input-field.component.spec.ts @@ -4,13 +4,7 @@ import { Directive, Input, } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { FeaturesConfig, @@ -28,6 +22,7 @@ import { ConfiguratorUISettingsConfig } from '../../../config/configurator-ui-se import { defaultConfiguratorUISettingsConfig } from '../../../config/default-configurator-ui-settings.config'; import { ConfiguratorAttributeCompositionContext } from '../../composition/configurator-attribute-composition.model'; import { ConfiguratorAttributeNumericInputFieldComponent } from './configurator-attribute-numeric-input-field.component'; +import { vi } from 'vitest'; import { ConfiguratorAttributeNumericInputFieldService, ConfiguratorAttributeNumericInterval, @@ -124,13 +119,13 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { }, }; - beforeEach(waitForAsync(() => { + beforeEach(async () => { configuratorUISettingsConfig.productConfigurator = defaultConfiguratorUISettingsConfig.productConfigurator; mockLanguageService = { getAll: () => of([]), - getActive: jasmine.createSpy().and.returnValue(of(locale)), - setActive: jasmine.createSpy(), + getActive: vi.fn().mockReturnValue(of(locale)), + setActive: vi.fn(), }; TestBed.configureTestingModule({ imports: [ @@ -172,7 +167,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -185,9 +180,8 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { component = fixture.componentInstance; component.attribute = structuredClone(attribute); component.language = locale; - fixture.detectChanges(); htmlElem = fixture.nativeElement; - spyOn( + vi.spyOn( configuratorAttributeNumericInputFieldService, 'getPatternForValidationMessage' ); @@ -195,10 +189,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { defaultConfiguratorUISettingsConfig.productConfigurator ?.updateDebounceTime?.input ?? component['FALLBACK_DEBOUNCE_TIME']; - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); }); function checkForValidity( @@ -207,7 +198,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { isValid: boolean ) { component.attribute.negativeAllowed = negativeAllowed; - component.ngOnInit(); + fixture.detectChanges(); component.attributeInputForm.setValue(input); checkForValidationMessage(component, fixture, htmlElem, isValid ? 0 : 1); } @@ -217,7 +208,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { numberOfValidationIssues: number ) { component.attribute = attributeInterval; - component.ngOnInit(); + fixture.detectChanges(); component.attributeInputForm.setValue(input); checkForValidationMessage( component, @@ -228,9 +219,17 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { } it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should not consider empty required input field as invalid, despite that it will be marked as error on the UI, so that engine is still called', () => { component.attribute.required = true; fixture.detectChanges(); @@ -362,6 +361,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { }); it('should raise event in case input was changed', () => { + fixture.detectChanges(); component.onChange(); expect( component['configuratorCommonsService'].updateConfiguration @@ -377,42 +377,41 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ).toHaveBeenCalledTimes(0); }); - it('should delay emit inputValue for debounce period', fakeAsync(() => { - component.attributeInputForm.setValue('123'); + it('should delay emit inputValue for debounce period', async () => { fixture.detectChanges(); + component.attributeInputForm.setValue('123'); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalled(); - })); + }); - it('should delay emit inputValue for debounce period in case ui settings config is missing, because it falls back to default time', fakeAsync(() => { + it('should delay emit inputValue for debounce period in case ui settings config is missing, because it falls back to default time', async () => { configuratorUISettingsConfig.productConfigurator = undefined; - component.attributeInputForm.setValue('123'); fixture.detectChanges(); + component.attributeInputForm.setValue('123'); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalled(); - })); + }); - it('should only emit once with last value if inputValue is changed within debounce period', fakeAsync(() => { - component.attributeInputForm.setValue('123'); + it('should only emit once with last value if inputValue is changed within debounce period', async () => { fixture.detectChanges(); - tick(DEBOUNCE_TIME / 2); + component.attributeInputForm.setValue('123'); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME / 2); component.attributeInputForm.setValue('123456'); - fixture.detectChanges(); - tick(DEBOUNCE_TIME / 2); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME / 2); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalledWith( @@ -424,37 +423,36 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { }, Configurator.UpdateType.ATTRIBUTE ); - })); + }); - it('should emit twice if inputValue is changed after debounce period', fakeAsync(() => { - component.attributeInputForm.setValue('123'); + it('should emit twice if inputValue is changed after debounce period', async () => { fixture.detectChanges(); - tick(DEBOUNCE_TIME); + component.attributeInputForm.setValue('123'); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); component.attributeInputForm.setValue('123456'); - fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).toHaveBeenCalledTimes(2); - })); + }); - it('should not emit inputValue after destroy', fakeAsync(() => { - component.attributeInputForm.setValue('123'); + it('should not emit inputValue after destroy', async () => { fixture.detectChanges(); + component.attributeInputForm.setValue('123'); component.ngOnDestroy(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect( component['configuratorCommonsService'].updateConfiguration ).not.toHaveBeenCalled(); - })); + }); describe('Accessibility', () => { - it("should contain input element with class name 'form-control' and 'aria-describedby' attribute attribute that indicates the ID of the element that describe the elements", fakeAsync(() => { + it("should contain input element with class name 'form-control' and 'aria-describedby' attribute attribute that indicates the ID of the element that describe the elements", async () => { component.attribute.userInput = '123'; fixture.detectChanges(); component.ngOnInit(); htmlElem = fixture.debugElement.nativeElement; - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -464,17 +462,18 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { 'aria-describedby', 'cx-configurator--label--attributeName' ); - })); + }); - it("should contain div element with class name 'cx-validation-msg' and 'aria-live' attribute that enables the screen reader to read out a error as soon as it occurs", fakeAsync(() => { + it("should contain div element with class name 'cx-validation-msg' and 'aria-live' attribute that enables the screen reader to read out a error as soon as it occurs", async () => { component.attribute.userInput = '123'; + fixture.detectChanges(); component.attributeInputForm.markAsTouched({ onlySelf: true }); component.attributeInputForm.setErrors({ wrongFormat: true, }); fixture.detectChanges(); - component.ngOnInit(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -484,17 +483,18 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { 'aria-live', 'assertive' ); - })); + }); - it("should contain div element with class name 'cx-validation-msg' and 'aria-atomic' attribute that indicates whether a screen reader will present a changed region based on the change notifications defined by the aria-relevant attribute", fakeAsync(() => { + it("should contain div element with class name 'cx-validation-msg' and 'aria-atomic' attribute that indicates whether a screen reader will present a changed region based on the change notifications defined by the aria-relevant attribute", async () => { component.attribute.userInput = '123'; + fixture.detectChanges(); component.attributeInputForm.markAsTouched({ onlySelf: true }); component.attributeInputForm.setErrors({ wrongFormat: true, }); fixture.detectChanges(); - component.ngOnInit(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -504,7 +504,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { 'aria-atomic', 'true' ); - })); + }); }); describe('getIntervalText', () => { @@ -522,9 +522,9 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { let minValueFormatted = '5.00'; let maxValueFormatted = '7.00'; - it('should return aria text for standard interval', fakeAsync(() => { + it('should return aria text for standard interval', async () => { fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericIntervalStandard maxValue:' + @@ -532,13 +532,13 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ' minValue:' + minValueFormatted ); - })); + }); - it('should return aria text for half open interval, upper value not included', fakeAsync(() => { + it('should return aria text for half open interval, upper value not included', async () => { interval.minValueIncluded = true; interval.maxValueIncluded = false; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericIntervalStandard maxValue:' + @@ -548,13 +548,13 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ' ' + 'configurator.a11y.numericIntervalStandardUpperEndpointNotIncluded' ); - })); + }); - it('should return aria text for half open interval, lower value not included', fakeAsync(() => { + it('should return aria text for half open interval, lower value not included', async () => { interval.minValueIncluded = false; interval.maxValueIncluded = true; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericIntervalStandard maxValue:' + @@ -564,13 +564,13 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ' ' + 'configurator.a11y.numericIntervalStandardLowerEndpointNotIncluded' ); - })); + }); - it('should return aria text for open interval', fakeAsync(() => { + it('should return aria text for open interval', async () => { interval.minValueIncluded = false; interval.maxValueIncluded = false; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericIntervalStandard maxValue:' + @@ -580,74 +580,74 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ' ' + 'configurator.a11y.numericIntervalStandardOpen' ); - })); + }); - it('should return aria text for infinite interval with min value', fakeAsync(() => { + it('should return aria text for infinite interval with min value', async () => { interval.minValue = 5; interval.maxValue = undefined; interval.minValueIncluded = false; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericInfiniteIntervalMinValue value:' + minValueFormatted ); - })); + }); - it('should return aria text for infinite interval with min value included', fakeAsync(() => { + it('should return aria text for infinite interval with min value included', async () => { interval.minValue = 5; interval.maxValue = undefined; interval.minValueIncluded = true; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericInfiniteIntervalMinValueIncluded value:' + minValueFormatted ); - })); + }); - it('should return aria text for infinite interval with max value', fakeAsync(() => { + it('should return aria text for infinite interval with max value', async () => { interval.minValue = undefined; interval.maxValue = 7; interval.maxValueIncluded = false; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericInfiniteIntervalMaxValue value:' + maxValueFormatted ); - })); + }); - it('should return aria text for infinite interval with max value included', fakeAsync(() => { + it('should return aria text for infinite interval with max value included', async () => { interval.minValue = undefined; interval.maxValue = 7; interval.maxValueIncluded = true; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericInfiniteIntervalMaxValueIncluded value:' + maxValueFormatted ); - })); + }); - it('should return text for single value', fakeAsync(() => { + it('should return text for single value', async () => { interval.minValue = 5; interval.maxValue = 5; interval.minValueIncluded = false; interval.maxValueIncluded = false; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component['getIntervalText'](interval)).toBe( 'configurator.a11y.numericIntervalSingleValue value:' + minValueFormatted ); - })); + }); }); describe('getIntervalTexts', () => { @@ -668,13 +668,13 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { let maxValue1Formatted = '7.00'; let minValue2Formatted = '10.00'; - it('should return concatenated aria text for multiple intervals', fakeAsync(() => { + it('should return concatenated aria text for multiple intervals', async () => { component.intervals = []; component.intervals.push(interval1); component.intervals.push(interval2); fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component.getHelpTextForInterval()).toBe( 'configurator.a11y.combinedIntervalsText combinedInterval:' + @@ -686,9 +686,9 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { 'configurator.a11y.numericInfiniteIntervalMinValueIncluded value:' + minValue2Formatted ); - })); + }); - it('should return concatenated aria text for multiple intervals with single value', fakeAsync(() => { + it('should return concatenated aria text for multiple intervals with single value', async () => { let interval3: ConfiguratorAttributeNumericInterval = { minValue: 12, maxValue: 12, @@ -702,7 +702,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { component.intervals.push(interval1); component.intervals.push(interval3); fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); expect(component.getHelpTextForInterval()).toBe( 'configurator.a11y.combinedIntervalsText combinedInterval:' + @@ -713,7 +713,7 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ' newInterval:configurator.a11y.numericIntervalSingleValue value:' + minValue3Formatted ); - })); + }); }); describe('getAriaLabelComplete', () => { @@ -731,14 +731,14 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { let minValueFormatted = '5.00'; let maxValueFormatted = '7.00'; - it('should return aria text for entered value including text for standard interval', fakeAsync(() => { - component.intervals = []; - component.intervals.push(interval); + it('should return aria text for entered value including text for standard interval', async () => { component.attribute.intervalInDomain = true; component.attribute.label = 'Intervaltest'; component.attribute.userInput = '123'; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); + // Set intervals AFTER detectChanges (which calls ngOnInit and resets intervals) + component.intervals = [interval]; expect(component.getAriaLabelComplete()).toBe( 'configurator.a11y.valueOfAttributeFull attribute:' + @@ -752,20 +752,20 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { ' minValue:' + minValueFormatted ); - })); + }); - it('should return aria text for blank value including text for infinite interval with min value', fakeAsync(() => { + it('should return aria text for blank value including text for infinite interval with min value', async () => { interval.minValue = 5; interval.maxValue = undefined; interval.minValueIncluded = false; - component.intervals = []; - component.intervals.push(interval); component.attribute.intervalInDomain = true; component.attribute.label = 'Intervaltest'; component.attribute.userInput = ''; fixture.detectChanges(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); + // Set intervals AFTER detectChanges (which calls ngOnInit and resets intervals) + component.intervals = [interval]; expect(component.getAriaLabelComplete()).toBe( 'configurator.a11y.valueOfAttributeBlank attribute:' + @@ -774,6 +774,6 @@ describe('ConfigAttributeNumericInputFieldComponent', () => { 'configurator.a11y.numericInfiniteIntervalMinValue value:' + minValueFormatted ); - })); + }); }); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/radio-button/configurator-attribute-radio-button.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/radio-button/configurator-attribute-radio-button.component.spec.ts index 0cc7f33b7f5..5b33db32624 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/radio-button/configurator-attribute-radio-button.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/radio-button/configurator-attribute-radio-button.component.spec.ts @@ -4,17 +4,16 @@ import { Directive, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { StoreModule } from '@ngrx/store'; import { I18nTestingModule } from '@spartacus/core'; import { FocusDirective, ItemCounterComponent } from '@spartacus/storefront'; -import { CONFIGURATOR_FEATURE } from '../../../../core/state/configurator-state'; -import { getConfiguratorReducers } from '../../../../core/state/reducers'; import { ConfiguratorShowMoreComponent } from '@spartacus/product-configurator/rulebased'; import { Observable, of } from 'rxjs'; import { CommonConfiguratorTestUtilsService } from '../../../../../common/testing/common-configurator-test-utils.service'; +import { ConfiguratorCommonsService } from '../../../../core/facade/configurator-commons.service'; import { ConfiguratorGroupsService } from '../../../../core/facade/configurator-groups.service'; import { Configurator } from '../../../../core/model/configurator.model'; import { ConfiguratorTestUtils } from '../../../../testing/configurator-test-utils'; @@ -81,6 +80,13 @@ class MockConfiguratorShowMoreComponent { @Input() productName: string; } +class MockConfiguratorCommonsService { + isConfigurationLoading(): Observable { + return of(false); + } + updateConfiguration(): void {} +} + const isCartEntryOrGroupVisited = true; class MockConfigUtilsService { isCartEntryOrGroupVisited(): Observable { @@ -108,7 +114,7 @@ describe('ConfigAttributeRadioButtonComponent', () => { let value3: Configurator.Value; let values: Configurator.Value[]; - beforeEach(waitForAsync(() => { + beforeEach(async () => { value1 = createValue('1', 'val1', true); value2 = createValue('2', VALUE_NAME_2, false); value3 = createValue('3', 'val3', false); @@ -129,14 +135,16 @@ describe('ConfigAttributeRadioButtonComponent', () => { I18nTestingModule, ReactiveFormsModule, StoreModule.forRoot({}), - StoreModule.forFeature(CONFIGURATOR_FEATURE, getConfiguratorReducers), ConfiguratorAttributeRadioButtonComponent, ConfiguratorAttributeInputFieldComponent, ConfiguratorAttributeNumericInputFieldComponent, ItemCounterComponent, ], providers: [ - ConfiguratorStorefrontUtilsService, + { + provide: ConfiguratorCommonsService, + useClass: MockConfiguratorCommonsService, + }, { provide: ConfiguratorGroupsService, useClass: MockGroupService, @@ -171,7 +179,7 @@ describe('ConfigAttributeRadioButtonComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -193,7 +201,6 @@ describe('ConfigAttributeRadioButtonComponent', () => { }; component.ownerKey = ownerKey; - fixture.detectChanges(); }); afterEach(() => { @@ -201,10 +208,12 @@ describe('ConfigAttributeRadioButtonComponent', () => { }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it('should set selectedSingleValue on init', () => { + fixture.detectChanges(); expect(component.attributeRadioButtonForm.value).toEqual( initialSelectedValue ); @@ -305,6 +314,7 @@ describe('ConfigAttributeRadioButtonComponent', () => { }); it('should not render description in case description not present on model', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -326,6 +336,7 @@ describe('ConfigAttributeRadioButtonComponent', () => { describe('Accessibility', () => { it("should contain input element with class name 'form-check-input' and 'aria-label' attribute that defines an accessible name to label the current unselected element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -359,6 +370,7 @@ describe('ConfigAttributeRadioButtonComponent', () => { }); it("should contain input element with class name 'form-check-input' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -371,6 +383,7 @@ describe('ConfigAttributeRadioButtonComponent', () => { }); it("should contain label element with class name 'form-check-label' and 'aria-hidden' attribute that removes label from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/read-only/configurator-attribute-read-only.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/read-only/configurator-attribute-read-only.component.spec.ts index e42dd5c7973..c3b869a3017 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/read-only/configurator-attribute-read-only.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/read-only/configurator-attribute-read-only.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { I18nTestingModule } from '@spartacus/core'; import { ConfiguratorShowMoreComponent } from '@spartacus/product-configurator/rulebased'; @@ -78,7 +78,7 @@ describe('ConfigAttributeReadOnlyComponent', () => { let htmlElem: HTMLElement; let configuratorPriceComponentOptions: ConfiguratorPriceComponentOptions; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.overrideComponent(ConfiguratorAttributeReadOnlyComponent, { set: { providers: [ @@ -119,7 +119,7 @@ describe('ConfigAttributeReadOnlyComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorAttributeReadOnlyComponent); @@ -133,7 +133,6 @@ describe('ConfigAttributeReadOnlyComponent', () => { selectedSingleValue: 'selectedValue', quantity: 1, }; - fixture.detectChanges(); myValues = structuredClone(allValues); configuratorPriceComponentOptions = { quantity: myValues[0].quantity, @@ -144,6 +143,7 @@ describe('ConfigAttributeReadOnlyComponent', () => { }); it('should create component', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -215,10 +215,13 @@ describe('ConfigAttributeReadOnlyComponent', () => { describe('no static Domain', () => { beforeEach(() => { component.attribute.selectedSingleValue = myValues[1].valueCode; - fixture.detectChanges(); }); describe('should display selectedSingleValue', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it("should contain span element with class name 'cx-visually-hidden' that hides label content on the UI", () => { CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -287,6 +290,7 @@ describe('ConfigAttributeReadOnlyComponent', () => { describe('rendering description at value level', () => { it('should not render description in case no desciption present on model', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle-dropdown/configurator-attribute-single-selection-bundle-dropdown.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle-dropdown/configurator-attribute-single-selection-bundle-dropdown.component.spec.ts index 86f0ed85175..d7f182effd2 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle-dropdown/configurator-attribute-single-selection-bundle-dropdown.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle-dropdown/configurator-attribute-single-selection-bundle-dropdown.component.spec.ts @@ -4,7 +4,7 @@ import { Directive, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -17,9 +17,8 @@ import { UrlTestingModule } from 'core-libs/core/src/routing/configurable-routes import { MockFeatureLevelDirective } from 'core-libs/storefront/shared/test/mock-feature-level-directive'; import { Observable, of } from 'rxjs'; import { CommonConfiguratorTestUtilsService } from '../../../../../common/testing/common-configurator-test-utils.service'; +import { ConfiguratorCommonsService } from '../../../../core/facade/configurator-commons.service'; import { Configurator } from '../../../../core/model/configurator.model'; -import { CONFIGURATOR_FEATURE } from '../../../../core/state/configurator-state'; -import { getConfiguratorReducers } from '../../../../core/state/reducers'; import { ConfiguratorTestUtils } from '../../../../testing/configurator-test-utils'; import { ConfiguratorPriceComponent, @@ -87,6 +86,13 @@ export class MockFocusDirective { @Input('cxFocus') protected config: any; } +class MockConfiguratorCommonsService { + isConfigurationLoading(): Observable { + return of(false); + } + updateConfiguration(): void {} +} + let showRequiredErrorMessage: boolean; class MockConfigUtilsService { isCartEntryOrGroupVisited(): Observable { @@ -212,11 +218,10 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { values, }; - fixture.detectChanges(); return component; } - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -224,12 +229,15 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { I18nTestingModule, UrlTestingModule, StoreModule.forRoot({}), - StoreModule.forFeature(CONFIGURATOR_FEATURE, getConfiguratorReducers), ConfiguratorAttributeSingleSelectionBundleDropdownComponent, ConfiguratorShowMoreComponent, ], providers: [ { provide: ActivatedRoute, useValue: new MockActivatedRoute({}) }, + { + provide: ConfiguratorCommonsService, + useClass: MockConfiguratorCommonsService, + }, { provide: ConfiguratorAttributeCompositionContext, useValue: ConfiguratorTestUtils.getAttributeContext(), @@ -275,7 +283,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { } ) .compileComponents(); - })); + }); afterEach(() => { fixture?.destroy(); @@ -284,6 +292,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { it('should create', () => { createComponentWithData(); + fixture.detectChanges(); expect(component).toBeTruthy(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -294,6 +303,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { it('should render an empty component in case showRequiredErrorMessage$ is `false`', () => { createComponentWithData(false).ngOnInit(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -308,7 +318,10 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { it('should show product card when product selected', () => { createComponentWithData(); - component.selectionValue = values[1]; + // values[0] is retract and selected=true; values[1] is non-retract and selected=true + // Make values[0] unselected so ngOnInit picks values[1] as the selection + values[0].selected = false; + component.attribute = { ...component.attribute, values }; fixture.detectChanges(); const card = htmlElem.querySelector( @@ -363,6 +376,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { describe('Accessibility', () => { it("should contain label element with class name 'cx-visually-hidden' that hides label content on the UI", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -377,6 +391,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { }); it("should contain select element with class name 'form-control' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -389,6 +404,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { }); it("should contain option elements with 'aria-label' attribute for value without price that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -422,8 +438,8 @@ describe('ConfiguratorAttributeSingleSelectionBundleDropdownComponent', () => { }); it('should return `true` in case value is not `###RETRACT_VALUE_CODE###`', () => { - component.selectionValue = values[1]; fixture.detectChanges(); + component.selectionValue = values[1]; expect(component.isNotRetractValue()).toBe(true); }); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle/configurator-attribute-single-selection-bundle.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle/configurator-attribute-single-selection-bundle.component.spec.ts index ce547f25cb5..71ac3785bff 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle/configurator-attribute-single-selection-bundle.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-bundle/configurator-attribute-single-selection-bundle.component.spec.ts @@ -1,12 +1,14 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { StoreModule } from '@ngrx/store'; import { I18nTestingModule, ProductConnector } from '@spartacus/core'; import { ItemCounterComponent } from '@spartacus/storefront'; +import { Observable, of } from 'rxjs'; import { CommonConfiguratorTestUtilsService } from '../../../../../common/testing/common-configurator-test-utils.service'; +import { ConfiguratorCommonsService } from '../../../../core/facade/configurator-commons.service'; import { Configurator } from '../../../../core/model/configurator.model'; import { CONFIGURATOR_FEATURE } from '../../../../core/state/configurator-state'; import { getConfiguratorReducers } from '../../../../core/state/reducers'; @@ -15,6 +17,7 @@ import { ConfiguratorPriceComponent, ConfiguratorPriceComponentOptions, } from '../../../price/configurator-price.component'; +import { ConfiguratorStorefrontUtilsService } from '../../../service/configurator-storefront-utils.service'; import { ConfiguratorShowMoreComponent } from '../../../show-more/configurator-show-more.component'; import { ConfiguratorAttributeCompositionContext } from '../../composition/configurator-attribute-composition.model'; import { @@ -27,6 +30,19 @@ import { } from '../../quantity/configurator-attribute-quantity.component'; import { ConfiguratorAttributeSingleSelectionBundleComponent } from './configurator-attribute-single-selection-bundle.component'; +class MockConfiguratorCommonsService { + updateConfiguration(): void {} + isConfigurationLoading(): Observable { + return of(false); + } +} + +class MockConfiguratorStorefrontUtilsService { + isCartEntryOrGroupVisited(): Observable { + return of(false); + } +} + @Component({ selector: 'cx-configurator-attribute-product-card', template: '', @@ -108,7 +124,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleComponent', () => { return value; }; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -126,6 +142,14 @@ describe('ConfiguratorAttributeSingleSelectionBundleComponent', () => { useValue: ConfiguratorTestUtils.getAttributeContext(), }, { provide: ProductConnector, useClass: MockProductConnector }, + { + provide: ConfiguratorCommonsService, + useClass: MockConfiguratorCommonsService, + }, + { + provide: ConfiguratorStorefrontUtilsService, + useClass: MockConfiguratorStorefrontUtilsService, + }, ], }) .overrideComponent(ConfiguratorAttributeSingleSelectionBundleComponent, { @@ -152,7 +176,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { values = [ @@ -211,8 +235,6 @@ describe('ConfiguratorAttributeSingleSelectionBundleComponent', () => { values, dataType: Configurator.DataType.USER_SELECTION_QTY_ATTRIBUTE_LEVEL, }; - - fixture.detectChanges(); }); afterEach(() => { @@ -220,6 +242,7 @@ describe('ConfiguratorAttributeSingleSelectionBundleComponent', () => { }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); diff --git a/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-image/configurator-attribute-single-selection-image.component.spec.ts b/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-image/configurator-attribute-single-selection-image.component.spec.ts index 796674f3646..70338aa3c4f 100644 --- a/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-image/configurator-attribute-single-selection-image.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/attribute/types/single-selection-image/configurator-attribute-single-selection-image.component.spec.ts @@ -4,7 +4,7 @@ import { Directive, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -38,6 +38,7 @@ import { ConfiguratorStorefrontUtilsService } from '../../../service/configurato import { ConfiguratorAttributeCompositionContext } from '../../composition/configurator-attribute-composition.model'; import { ConfiguratorAttributePriceChangeService } from '../../price-change/configurator-attribute-price-change.service'; import { ConfiguratorAttributeSingleSelectionImageComponent } from './configurator-attribute-single-selection-image.component'; +import { vi } from 'vitest'; const VALUE_DISPLAY_NAME = 'val2'; class MockGroupService {} @@ -80,7 +81,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { const groupId = 'testGroup'; const attributeName = 'attributeName'; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.overrideComponent( ConfiguratorAttributeSingleSelectionImageComponent, {} @@ -140,7 +141,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { }, }) .compileComponents(); - })); + }); function createImage(url: string, altText: string): Configurator.Image { const configImage: Configurator.Image = { @@ -198,10 +199,10 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { values: values, }; component.ownerKey = ownerKey; - fixture.detectChanges(); }); it('should create a component', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -230,7 +231,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { By.css('cx-popover > .popover-body > span') ); expect(description).toBeTruthy(); - expect(description.nativeElement.innerText).toBe( + expect(description.nativeElement.textContent?.trim()).toBe( (component.attribute.values ?? [{}])[1].description ); infoButton.click(); // hide popover after test again @@ -258,10 +259,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { describe('select single image', () => { it('should not call service for update and in case attribute is read-only', () => { - spyOn( - component['configuratorCommonsService'], - 'updateConfiguration' - ).and.callThrough(); + vi.spyOn(component['configuratorCommonsService'], 'updateConfiguration'); component.attribute.uiType = Configurator.UiType.READ_ONLY_SINGLE_SELECTION_IMAGE; value2.selected = true; @@ -309,6 +307,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { describe('Accessibility', () => { it("should contain input element with class name 'form-input' and 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -324,6 +323,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { }); it("should contain input element with class name 'form-input' and 'aria-describedby' attribute that indicates the ID of the element that describe the elements", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -336,6 +336,7 @@ describe('ConfiguratorAttributeSingleSelectionImageComponent', () => { }); it("should contain input elements with class name 'form-input' and 'checked' attribute that indicates the current 'checked' state of widgete", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/configurator-conflict-and-error-messages/configurator-conflict-and-error-messages.component.spec.ts b/feature-libs/product-configurator/rulebased/components/configurator-conflict-and-error-messages/configurator-conflict-and-error-messages.component.spec.ts index 6d36c21c26d..720407e82b5 100644 --- a/feature-libs/product-configurator/rulebased/components/configurator-conflict-and-error-messages/configurator-conflict-and-error-messages.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/configurator-conflict-and-error-messages/configurator-conflict-and-error-messages.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { I18nTestingModule } from '@spartacus/core'; @@ -111,7 +111,7 @@ describe('ConfiguratorConflictAndErrorMessagesComponent', () => { let configuratorUtils: CommonConfiguratorUtilsService; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -135,7 +135,7 @@ describe('ConfiguratorConflictAndErrorMessagesComponent', () => { }, add: { imports: [MockCxIconComponent] }, }); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( ConfiguratorConflictAndErrorMessagesComponent diff --git a/feature-libs/product-configurator/rulebased/components/conflict-description/configurator-conflict-description.component.spec.ts b/feature-libs/product-configurator/rulebased/components/conflict-description/configurator-conflict-description.component.spec.ts index 138d7a87c2f..1946fe66fdc 100644 --- a/feature-libs/product-configurator/rulebased/components/conflict-description/configurator-conflict-description.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/conflict-description/configurator-conflict-description.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ICON_TYPE } from '@spartacus/storefront'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { Configurator } from '../../core/model/configurator.model'; @@ -19,7 +19,7 @@ describe('ConfigurationConflictDescriptionComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorConflictDescriptionComponent, MockCxIconComponent], providers: [], @@ -30,7 +30,7 @@ describe('ConfigurationConflictDescriptionComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorConflictDescriptionComponent); diff --git a/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog-launcher.service.spec.ts b/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog-launcher.service.spec.ts index 308d2063eca..33154d07f17 100644 --- a/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog-launcher.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog-launcher.service.spec.ts @@ -1,7 +1,8 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ElementRef } from '@angular/core'; import { LaunchDialogService, LAUNCH_CALLER } from '@spartacus/storefront'; -import { Observable, of, Subject } from 'rxjs'; +import { firstValueFrom, Observable, of, Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { ConfiguratorConflictSolverDialogLauncherService } from './configurator-conflict-solver-dialog-launcher.service'; import { CommonConfigurator, @@ -13,6 +14,7 @@ import { ConfiguratorGroupsService } from '../../core/facade/configurator-groups import { Configurator } from '../../core/model/configurator.model'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; let lastDialogData: any; @@ -74,8 +76,8 @@ describe('ConfiguratorConflictSolverDialogLauncherService', () => { function initLauncherService() { listener = TestBed.inject(ConfiguratorConflictSolverDialogLauncherService); launchDialogService = TestBed.inject(LaunchDialogService); - spyOn(launchDialogService, 'closeDialog').and.stub(); - spyOn(launchDialogService, 'openDialogAndSubscribe').and.callThrough(); + vi.spyOn(launchDialogService, 'closeDialog').mockImplementation(() => {}); + vi.spyOn(launchDialogService, 'openDialogAndSubscribe'); } beforeEach(() => { @@ -104,6 +106,11 @@ describe('ConfiguratorConflictSolverDialogLauncherService', () => { afterEach(() => { listener.ngOnDestroy(); + vi.useRealTimers(); + }); + + beforeEach(() => { + vi.useFakeTimers(); }); describe('conflictGroups observable', () => { @@ -114,68 +121,70 @@ describe('ConfiguratorConflictSolverDialogLauncherService', () => { configRouterData.pageType = ConfiguratorRouter.PageType.CONFIGURATION; }); - it('should emit group data', (done) => { + it('should emit group data', async () => { routerData$ = of(configRouterData); initLauncherService(); - listener.conflictGroupAndRouterData$.subscribe((data) => { - expect(data.conflictGroup).toEqual(group); - done(); + let data: any; + listener.conflictGroupAndRouterData$.pipe(take(1)).subscribe((d) => { + data = d; }); groupSubject.next(group); + await vi.advanceTimersByTimeAsync(0); + expect(data.conflictGroup).toEqual(group); }); }); describe('controlDialog', () => { - it('should open conflict solver dialog because there are some conflict groups', fakeAsync(() => { + it('should open conflict solver dialog because there are some conflict groups', async () => { initLauncherService(); groupSubject.next(group); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); - })); + }); - it('should open conflict solver dialog only once if same conflict groups is emitted', fakeAsync(() => { + it('should open conflict solver dialog only once if same conflict groups is emitted', async () => { initLauncherService(); groupSubject.next(group); - tick(0); + await vi.advanceTimersByTimeAsync(0); groupSubject.next(group); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalledTimes( 1 ); - })); + }); - it('should close conflict solver dialog because there are not any conflict groups', fakeAsync(() => { + it('should close conflict solver dialog because there are not any conflict groups', async () => { initLauncherService(); groupSubject.next(group); - tick(0); + await vi.advanceTimersByTimeAsync(0); groupSubject.next(undefined); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.closeDialog).toHaveBeenCalled(); expect(launchDialogService.closeDialog).toHaveBeenCalledWith( 'CLOSE_NO_CONFLICTS_EXIST' ); - })); + }); - it('should NOT close conflict solver dialog because it has not been opened yet', fakeAsync(() => { + it('should NOT close conflict solver dialog because it has not been opened yet', async () => { initLauncherService(); groupSubject.next(undefined); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.closeDialog).not.toHaveBeenCalled(); - })); + }); - it('should close conflict solver dialog only once unless it is not opened again', fakeAsync(() => { + it('should close conflict solver dialog only once unless it is not opened again', async () => { initLauncherService(); groupSubject.next(group); - tick(0); + await vi.advanceTimersByTimeAsync(0); groupSubject.next(undefined); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.closeDialog).toHaveBeenCalledTimes(1); groupSubject.next(undefined); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.closeDialog).toHaveBeenCalledTimes(1); - })); + }); }); describe('closeModal', () => { diff --git a/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog.component.spec.ts b/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog.component.spec.ts index 89f39c4f3bf..00e43791232 100644 --- a/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/conflict-solver-dialog/configurator-conflict-solver-dialog.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Directive, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CxDatePipe, FeatureDirective, @@ -31,6 +31,7 @@ import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorGroupComponent } from '../group'; import { ConfiguratorStorefrontUtilsService } from './../service/configurator-storefront-utils.service'; import { ConfiguratorConflictSolverDialogComponent } from './configurator-conflict-solver-dialog.component'; +import { vi } from 'vitest'; export class MockIconFontLoaderService { getStyleClasses(_iconType: ICON_TYPE): void {} @@ -99,7 +100,7 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { let configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService; let focusService: KeyboardFocusService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [IconModule, ConfiguratorConflictSolverDialogComponent], providers: [ @@ -136,7 +137,7 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -144,7 +145,6 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { ); component = fixture.componentInstance; htmlElem = fixture.nativeElement; - fixture.detectChanges(); configuratorCommonsService = TestBed.inject( ConfiguratorCommonsService as Type @@ -154,20 +154,17 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { ConfiguratorStorefrontUtilsService as Type ); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'scrollToConfigurationElement' - ).and.callThrough(); + ); - spyOn( - configuratorStorefrontUtilsService, - 'focusFirstAttribute' - ).and.callThrough(); + vi.spyOn(configuratorStorefrontUtilsService, 'focusFirstAttribute'); - spyOn(configuratorCommonsService, 'updateConfiguration').and.callThrough(); + vi.spyOn(configuratorCommonsService, 'updateConfiguration'); launchDialogService = TestBed.inject(LaunchDialogService); - spyOn(launchDialogService, 'closeDialog').and.callThrough(); + vi.spyOn(launchDialogService, 'closeDialog'); focusService = TestBed.inject(KeyboardFocusService); }); @@ -178,6 +175,7 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { describe('Rendering', () => { it('should render a conflict solver dialog correctly', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -245,6 +243,7 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { describe('init', () => { it('should clear persisted focus key', () => { + fixture.detectChanges(); focusService.set('key'); component.init(NEVER, NEVER); expect(focusService.get()).toBeUndefined(); @@ -252,6 +251,7 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { }); describe('dismissModal', () => { it('should close dialog when dismissModal is called', () => { + fixture.detectChanges(); const reason = 'Close conflict solver dialog'; component.ngOnInit(); component.dismissModal(reason); @@ -266,6 +266,10 @@ describe('ConfiguratorConflictSolverDialogComponent', () => { }); describe('Accessibility', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + it("should contain action button element with class name 'close' and 'aria-label' attribute that indicates the text for close button", () => { CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, diff --git a/feature-libs/product-configurator/rulebased/components/conflict-suggestion/configurator-conflict-suggestion.component.spec.ts b/feature-libs/product-configurator/rulebased/components/conflict-suggestion/configurator-conflict-suggestion.component.spec.ts index 2d7ca7b78a3..fa818e841e2 100644 --- a/feature-libs/product-configurator/rulebased/components/conflict-suggestion/configurator-conflict-suggestion.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/conflict-suggestion/configurator-conflict-suggestion.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { Configurator } from '../../core/model/configurator.model'; @@ -11,7 +11,7 @@ describe('ConfigurationConflictSuggestionComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule, ConfiguratorConflictSuggestionComponent], providers: [], @@ -22,7 +22,7 @@ describe('ConfigurationConflictSuggestionComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorConflictSuggestionComponent); diff --git a/feature-libs/product-configurator/rulebased/components/exit-button/configurator-exit-button.component.spec.ts b/feature-libs/product-configurator/rulebased/components/exit-button/configurator-exit-button.component.spec.ts index cd8fefda1bd..2a2fe7af6c0 100644 --- a/feature-libs/product-configurator/rulebased/components/exit-button/configurator-exit-button.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/exit-button/configurator-exit-button.component.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { @@ -22,6 +22,7 @@ import { ConfiguratorCommonsService } from '../../core/facade/configurator-commo import { Configurator } from '../../core/model/configurator.model'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorExitButtonComponent } from './configurator-exit-button.component'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; const CART_ENTRY_KEY = '001+1'; @@ -87,7 +88,6 @@ function initialize() { fixture = TestBed.createComponent(ConfiguratorExitButtonComponent); component = fixture.componentInstance; htmlElem = fixture.nativeElement; - fixture.detectChanges(); } function setRouterTestDataCartEntry() { @@ -108,7 +108,7 @@ describe('ConfiguratorExitButton', () => { let routingService: RoutingService; let breakpointService: BreakpointService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -139,28 +139,27 @@ describe('ConfiguratorExitButton', () => { }, ], }); - })); + }); beforeEach(() => { - fixture = TestBed.createComponent(ConfiguratorExitButtonComponent); - component = fixture.componentInstance; routingService = TestBed.inject(RoutingService as Type); breakpointService = TestBed.inject( BreakpointService as Type ); - htmlElem = fixture.nativeElement; - fixture.detectChanges(); }); it('should create component', () => { + initialize(); + fixture.detectChanges(); expect(component).toBeDefined(); }); describe('exit a configuration', () => { it('should navigate to product detail page', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); setRouterTestDataProduct(); initialize(); + fixture.detectChanges(); component.exitConfiguration(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'product', @@ -169,9 +168,10 @@ describe('ConfiguratorExitButton', () => { }); it('should navigate back to cart', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); setRouterTestDataCartEntry(); initialize(); + fixture.detectChanges(); component.exitConfiguration(); expect(routingService.go).toHaveBeenCalledWith('cart'); }); @@ -179,10 +179,11 @@ describe('ConfiguratorExitButton', () => { describe('rendering tests', () => { it('should render short text in mobile mode', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); setRouterTestDataProduct(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -192,10 +193,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render long text tooltip in mobile mode', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); setRouterTestDataProduct(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -208,10 +210,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render long text in desktop mode', () => { - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); - spyOn(breakpointService, 'isDown').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(false)); setRouterTestDataProduct(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -221,10 +224,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render long text tooltip in desktop mode', () => { - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); - spyOn(breakpointService, 'isDown').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(false)); setRouterTestDataProduct(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -234,10 +238,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render short text when navigate from cart', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); setRouterTestDataCartEntry(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -247,10 +252,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render long text tooltip when navigate from cart in mobile mode', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); setRouterTestDataCartEntry(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -263,10 +269,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render long text when navigate from cart', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(false)); - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); setRouterTestDataCartEntry(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -276,10 +283,11 @@ describe('ConfiguratorExitButton', () => { }); it('should render long text tooltip when navigate from cart in desktop mode', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(false)); - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); setRouterTestDataCartEntry(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, 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 0a0033ed6ee..7ce842bc037 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 @@ -1,11 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, Type } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterState } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -37,6 +31,7 @@ import { productConfiguration } from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorGroupComponent } from '../group'; import { ConfiguratorFormComponent } from './configurator-form.component'; +import { vi } from 'vitest'; @Component({ selector: 'cx-configurator-group', @@ -269,7 +264,7 @@ let hasConfigurationConflictsObservable: Observable = EMPTY; let keyboardFocusService: KeyboardFocusService; describe('ConfiguratorFormComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -311,63 +306,48 @@ describe('ConfiguratorFormComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { configuratorGroupsService = TestBed.inject( ConfiguratorGroupsService as Type ); - spyOn(configuratorGroupsService, 'setGroupStatusVisited').and.callThrough(); - spyOn( - configuratorGroupsService, - 'navigateToConflictSolver' - ).and.callThrough(); + vi.spyOn(configuratorGroupsService, 'setGroupStatusVisited'); + vi.spyOn(configuratorGroupsService, 'navigateToConflictSolver'); - spyOn( - configuratorGroupsService, - 'navigateToFirstIncompleteGroup' - ).and.callThrough(); + vi.spyOn(configuratorGroupsService, 'navigateToFirstIncompleteGroup'); configuratorCommonsService = TestBed.inject( ConfiguratorCommonsService as Type ); - spyOn( - configuratorCommonsService, - 'isConfigurationLoading' - ).and.callThrough(); - spyOn( - configuratorCommonsService, - 'getOrCreateConfiguration' - ).and.callThrough(); - spyOn(configuratorCommonsService, 'getConfiguration').and.callThrough(); - spyOn( - configuratorCommonsService, - 'checkConflictSolverDialog' - ).and.callThrough(); + vi.spyOn(configuratorCommonsService, 'isConfigurationLoading'); + vi.spyOn(configuratorCommonsService, 'getOrCreateConfiguration'); + vi.spyOn(configuratorCommonsService, 'getConfiguration'); + vi.spyOn(configuratorCommonsService, 'checkConflictSolverDialog'); globalMessageService = TestBed.inject( GlobalMessageService as Type ); - spyOn(globalMessageService, 'add').and.callThrough(); + vi.spyOn(globalMessageService, 'add'); isConfigurationLoadingObservable = of(false); configExpertModeService = TestBed.inject( ConfiguratorExpertModeService as Type ); - spyOn(configExpertModeService, 'setExpModeRequested').and.callThrough(); + vi.spyOn(configExpertModeService, 'setExpModeRequested'); hasConfigurationConflictsObservable = of(false); launchDialogService = TestBed.inject( LaunchDialogService as Type ); - spyOn(launchDialogService, 'openDialogAndSubscribe').and.callThrough(); + vi.spyOn(launchDialogService, 'openDialogAndSubscribe'); keyboardFocusService = TestBed.inject( KeyboardFocusService as Type ); - spyOn(keyboardFocusService, 'clear').and.callThrough(); + vi.spyOn(keyboardFocusService, 'clear'); configuration = structuredClone(productConfiguration); }); @@ -535,6 +515,12 @@ describe('ConfiguratorFormComponent', () => { }); describe('ngOnInit()', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); it('should call getConfiguration in order to prepare conflict check', () => { routerStateObservable = mockRouterStateWithQueryParams({}); createComponentWithData(); @@ -552,7 +538,7 @@ describe('ConfiguratorFormComponent', () => { ).toHaveBeenCalledTimes(1); }); - it('should launch the restart config dialog with data if requested and when the config is not new', fakeAsync(() => { + it('should launch the restart config dialog with data if requested and when the config is not new', async () => { routerStateObservable = mockRouterStateWithQueryParams({ displayRestartDialog: 'true', }); @@ -560,25 +546,25 @@ describe('ConfiguratorFormComponent', () => { config.interactionState.newConfiguration = false; configurationCreateObservable = of(config); createComponentWithData(); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalledWith( LAUNCH_CALLER.CONFIGURATOR_RESTART_DIALOG, undefined, { owner: config.owner } ); - })); + }); - it('should NOT launch the restart config dialog if not requested and not a new config', fakeAsync(() => { + it('should NOT launch the restart config dialog if not requested and not a new config', async () => { routerStateObservable = mockRouterStateWithQueryParams({}); const config: Configurator.Configuration = structuredClone(configRead); config.interactionState.newConfiguration = false; configurationCreateObservable = of(config); createComponentWithData(); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.openDialogAndSubscribe).not.toHaveBeenCalled(); - })); + }); - it('should NOT launch the restart config dialog if requested but a new config', fakeAsync(() => { + it('should NOT launch the restart config dialog if requested but a new config', async () => { routerStateObservable = mockRouterStateWithQueryParams({ displayRestartDialog: 'true', }); @@ -586,9 +572,9 @@ describe('ConfiguratorFormComponent', () => { config.interactionState.newConfiguration = true; configurationCreateObservable = of(config); createComponentWithData(); - tick(0); + await vi.advanceTimersByTimeAsync(0); expect(launchDialogService.openDialogAndSubscribe).not.toHaveBeenCalled(); - })); + }); }); describe('listenForConflictResolution()', () => { diff --git a/feature-libs/product-configurator/rulebased/components/group-menu/configurator-group-menu.component.spec.ts b/feature-libs/product-configurator/rulebased/components/group-menu/configurator-group-menu.component.spec.ts index 59b519c542b..f6fd9407cd9 100644 --- a/feature-libs/product-configurator/rulebased/components/group-menu/configurator-group-menu.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/group-menu/configurator-group-menu.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Directive, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { Router, RouterState } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -25,7 +25,7 @@ import { IconComponent, ICON_TYPE, } from '@spartacus/storefront'; -import { NEVER, Observable, of } from 'rxjs'; +import { firstValueFrom, NEVER, Observable, of } from 'rxjs'; import { take } from 'rxjs/operators'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service'; @@ -50,6 +50,7 @@ import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from './../service/configurator-storefront-utils.service'; import { ConfiguratorGroupMenuComponent } from './configurator-group-menu.component'; import { ConfiguratorGroupMenuService } from './configurator-group-menu.component.service'; +import { vi } from 'vitest'; let mockGroupVisited = false; let mockDirection = DirectionMode.LTR; @@ -246,7 +247,7 @@ function initialize() { } describe('ConfiguratorGroupMenuComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -296,31 +297,41 @@ describe('ConfiguratorGroupMenuComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { configuratorGroupsService = TestBed.inject(ConfiguratorGroupsService); - spyOn(configuratorGroupsService, 'navigateToGroup').and.stub(); - spyOn(configuratorGroupsService, 'setMenuParentGroup').and.stub(); - spyOn(configuratorGroupsService, 'isGroupVisited').and.callThrough(); + vi.spyOn(configuratorGroupsService, 'navigateToGroup').mockImplementation( + () => {} + ); + vi.spyOn( + configuratorGroupsService, + 'setMenuParentGroup' + ).mockImplementation(() => {}); + vi.spyOn(configuratorGroupsService, 'isGroupVisited'); isConflictGroupType = false; - spyOn(configuratorGroupsService, 'isConflictGroupType').and.callThrough(); + vi.spyOn(configuratorGroupsService, 'isConflictGroupType'); hamburgerMenuService = TestBed.inject(HamburgerMenuService); - spyOn(hamburgerMenuService, 'toggle').and.stub(); + vi.spyOn(hamburgerMenuService, 'toggle').mockImplementation(() => {}); configUtils = TestBed.inject(ConfiguratorStorefrontUtilsService); - spyOn(configUtils, 'setFocus').and.stub(); - spyOn(configUtils, 'focusFirstActiveElement').and.stub(); + vi.spyOn(configUtils, 'setFocus').mockImplementation(() => {}); + vi.spyOn(configUtils, 'focusFirstActiveElement').mockImplementation( + () => {} + ); configuratorUtils = TestBed.inject(CommonConfiguratorUtilsService); configuratorUtils.setOwnerKey(mockProductConfiguration.owner); configGroupMenuService = TestBed.inject(ConfiguratorGroupMenuService); - spyOn(configGroupMenuService, 'switchGroupOnArrowPress').and.stub(); + vi.spyOn( + configGroupMenuService, + 'switchGroupOnArrowPress' + ).mockImplementation(() => {}); directionService = TestBed.inject(DirectionService); - spyOn(directionService, 'getDirection').and.callThrough(); + vi.spyOn(directionService, 'getDirection'); configExpertModeService = TestBed.inject(ConfiguratorExpertModeService); }); @@ -388,7 +399,7 @@ describe('ConfiguratorGroupMenuComponent', () => { it('should return 0 groups if menu parent group is first group', () => { productConfigurationObservable = of(mockProductConfiguration); routerStateObservable = of(mockRouterState); - spyOn(configuratorGroupsService, 'getMenuParentGroup').and.returnValue( + vi.spyOn(configuratorGroupsService, 'getMenuParentGroup').mockReturnValue( of(mockProductConfiguration.groups[0]) ); initialize(); @@ -973,11 +984,13 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should navigate back to parent group', () => { - spyOn(configuratorGroupsService, 'getMenuParentGroup').and.returnValue( + vi.spyOn(configuratorGroupsService, 'getMenuParentGroup').mockReturnValue( of(clonedProductConfiguration.groups[0]) ); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(true); - spyOn(configuratorGroupsService, 'getParentGroup').and.callThrough(); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( + true + ); + vi.spyOn(configuratorGroupsService, 'getParentGroup'); let event = new KeyboardEvent('keydown', { code: 'ArrowLeft', @@ -998,7 +1011,9 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should navigate to subgroups', () => { - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(false); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( + false + ); let event = new KeyboardEvent('keydown', { code: 'ArrowRight', @@ -1026,11 +1041,13 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should navigate back to parent group', () => { - spyOn(configuratorGroupsService, 'getMenuParentGroup').and.returnValue( + vi.spyOn(configuratorGroupsService, 'getMenuParentGroup').mockReturnValue( of(clonedProductConfiguration.groups[0]) ); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(true); - spyOn(configuratorGroupsService, 'getParentGroup').and.callThrough(); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( + true + ); + vi.spyOn(configuratorGroupsService, 'getParentGroup'); let event = new KeyboardEvent('keydown', { code: 'ArrowRight', @@ -1051,7 +1068,9 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should navigate to subgroups', () => { - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(false); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( + false + ); let event = new KeyboardEvent('keydown', { code: 'ArrowLeft', @@ -1189,163 +1208,139 @@ describe('ConfiguratorGroupMenuComponent', () => { mockGroupVisited = true; }); - it('should return appropriate (ICONSUCCESS) aria-describedby for variant configurator if group is complete and consistent', (done) => { + it('should return appropriate (ICONSUCCESS) aria-describedby for variant configurator if group is complete and consistent', async () => { clonedProductConfiguration.groups[1].complete = true; clonedProductConfiguration.groups[1].consistent = true; clonedProductConfiguration.owner.configuratorType = typeVariant; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[1], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONSUCCESS1234-56-7892 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONSUCCESS1234-56-7892 inListOfGroups' + ); }); - it('should return appropriate (only inListOfGroups) aria-describedby if group is complete, consistent and type is CPQ', (done) => { + it('should return appropriate (only inListOfGroups) aria-describedby if group is complete, consistent and type is CPQ', async () => { clonedProductConfiguration.groups[1].complete = true; clonedProductConfiguration.groups[1].consistent = true; clonedProductConfiguration.owner.configuratorType = typeCPQ; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[1], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual('inListOfGroups'); - done(); - }); + ); + expect(describedby.trim()).toEqual('inListOfGroups'); }); - it('should return appropriate (ICONWARNING) aria-describedby if group is inconsistent and type is variant', (done) => { + it('should return appropriate (ICONWARNING) aria-describedby if group is inconsistent and type is variant', async () => { clonedProductConfiguration.groups[0].complete = true; clonedProductConfiguration.groups[0].consistent = false; clonedProductConfiguration.owner.configuratorType = typeVariant; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONWARNING1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONWARNING1234-56-7891 inListOfGroups' + ); }); - it('should return appropriate (only inListOfGroups) if group is inconsistent and type is CPQ', (done) => { + it('should return appropriate (only inListOfGroups) if group is inconsistent and type is CPQ', async () => { clonedProductConfiguration.groups[0].complete = true; clonedProductConfiguration.groups[0].consistent = false; clonedProductConfiguration.owner.configuratorType = typeCPQ; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual('inListOfGroups'); - done(); - }); + ); + expect(describedby.trim()).toEqual('inListOfGroups'); }); - it('should return appropriate (ICONERROR) aria-describedby if group is incomplete, consistent and type is CPQ', (done) => { + it('should return appropriate (ICONERROR) aria-describedby if group is incomplete, consistent and type is CPQ', async () => { clonedProductConfiguration.groups[0].complete = false; clonedProductConfiguration.groups[0].consistent = true; clonedProductConfiguration.owner.configuratorType = typeCPQ; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONERROR1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONERROR1234-56-7891 inListOfGroups' + ); }); - it('should return appropriate (ICONERROR) aria-describedby if group is incomplete, consistent and type is variant', (done) => { + it('should return appropriate (ICONERROR) aria-describedby if group is incomplete, consistent and type is variant', async () => { clonedProductConfiguration.groups[0].complete = false; clonedProductConfiguration.groups[0].consistent = true; clonedProductConfiguration.owner.configuratorType = typeVariant; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONERROR1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONERROR1234-56-7891 inListOfGroups' + ); }); - it('should return appropriate (ICONWARNING and ICONERROR) aria-describedby if group is incomplete, inconsistent and type is variant', (done) => { + it('should return appropriate (ICONWARNING and ICONERROR) aria-describedby if group is incomplete, inconsistent and type is variant', async () => { clonedProductConfiguration.groups[0].complete = false; clonedProductConfiguration.groups[0].consistent = false; clonedProductConfiguration.owner.configuratorType = typeVariant; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONWARNING1234-56-7891 ICONERROR1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONWARNING1234-56-7891 ICONERROR1234-56-7891 inListOfGroups' + ); }); - it('should return appropriate (ICONERROR) aria-describedby if group is incomplete, inconsistent and type is variant', (done) => { + it('should return appropriate (ICONERROR) aria-describedby if group is incomplete, inconsistent and type is variant', async () => { clonedProductConfiguration.groups[0].complete = false; clonedProductConfiguration.groups[0].consistent = false; clonedProductConfiguration.owner.configuratorType = typeCPQ; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONERROR1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONERROR1234-56-7891 inListOfGroups' + ); }); - it('should return appropriate (ICONCARET_RIGHT) aria-describedby if group has subgroups', (done) => { + it('should return appropriate (ICONCARET_RIGHT) aria-describedby if group has subgroups', async () => { clonedProductConfiguration.owner.configuratorType = 'CONFIGURATOR'; clonedProductConfiguration.groups[0].complete = true; clonedProductConfiguration.groups[0].consistent = true; @@ -1355,21 +1350,18 @@ describe('ConfiguratorGroupMenuComponent', () => { isConflictGroupType = false; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONSUCCESS1234-56-7891 ICONCARET_RIGHT1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONSUCCESS1234-56-7891 ICONCARET_RIGHT1234-56-7891 inListOfGroups' + ); }); - it('should return appropriate (ICONCARET_RIGHT and ICONERROR) aria-describedby if group has subgroups', (done) => { + it('should return appropriate (ICONCARET_RIGHT and ICONERROR) aria-describedby if group has subgroups', async () => { clonedProductConfiguration.groups[0].groupType = undefined; clonedProductConfiguration.groups[0].complete = false; clonedProductConfiguration.groups[0].consistent = false; @@ -1379,18 +1371,15 @@ describe('ConfiguratorGroupMenuComponent', () => { isConflictGroupType = false; initialize(); - component - .getAriaDescribedby( + const describedby = await firstValueFrom( + component.getAriaDescribedby( clonedProductConfiguration.groups[0], clonedProductConfiguration ) - .pipe(take(1)) - .subscribe((describedby) => { - expect(describedby.trim()).toEqual( - 'ICONERROR1234-56-7891 ICONCARET_RIGHT1234-56-7891 inListOfGroups' - ); - done(); - }); + ); + expect(describedby.trim()).toEqual( + 'ICONERROR1234-56-7891 ICONCARET_RIGHT1234-56-7891 inListOfGroups' + ); }); }); @@ -1515,7 +1504,7 @@ describe('ConfiguratorGroupMenuComponent', () => { }); describe('displayMenuItem', () => { - it('should display conflict header menu item', (done) => { + it('should display conflict header menu item', async () => { let configurationWithConflicts = structuredClone( productConfigurationWithConflicts ); @@ -1524,16 +1513,13 @@ describe('ConfiguratorGroupMenuComponent', () => { routerStateObservable = of(mockRouterState); initialize(); - component - .displayMenuItem(configurationWithConflicts.groups[0]) - .pipe(take(1)) - .subscribe((displayMenuItem) => { - expect(displayMenuItem).toBe(true); - done(); - }); + const displayMenuItem = await firstValueFrom( + component.displayMenuItem(configurationWithConflicts.groups[0]) + ); + expect(displayMenuItem).toBe(true); }); - it('should not display conflict header menu item', (done) => { + it('should not display conflict header menu item', async () => { let configurationWithConflicts = structuredClone( productConfigurationWithConflicts ); @@ -1543,13 +1529,10 @@ describe('ConfiguratorGroupMenuComponent', () => { routerStateObservable = of(mockRouterState); initialize(); - component - .displayMenuItem(configurationWithConflicts.groups[0]) - .pipe(take(1)) - .subscribe((displayMenuItem) => { - expect(displayMenuItem).toBe(false); - done(); - }); + const displayMenuItem = await firstValueFrom( + component.displayMenuItem(configurationWithConflicts.groups[0]) + ); + expect(displayMenuItem).toBe(false); }); }); @@ -1564,7 +1547,9 @@ describe('ConfiguratorGroupMenuComponent', () => { const event = new KeyboardEvent('keydown', { code: 'Tab', }); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.stub(); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockImplementation( + () => {} + ); initialize(); component['handleFocusLoopInMobileMode'](event); @@ -1576,7 +1561,9 @@ describe('ConfiguratorGroupMenuComponent', () => { const event = new KeyboardEvent('keydown', { code: 'ArrowUp', }); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.stub(); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockImplementation( + () => {} + ); initialize(); component['handleFocusLoopInMobileMode'](event); @@ -1589,7 +1576,9 @@ describe('ConfiguratorGroupMenuComponent', () => { code: 'Tab', shiftKey: true, }); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.stub(); + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockImplementation( + () => {} + ); initialize(); component['handleFocusLoopInMobileMode'](event); @@ -1601,10 +1590,13 @@ describe('ConfiguratorGroupMenuComponent', () => { const event = new KeyboardEvent('keydown', { code: 'Tab', }); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(true); - spyOn(configGroupMenuService, 'isActiveGroupInGroupList').and.returnValue( - false + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( + true ); + vi.spyOn( + configGroupMenuService, + 'isActiveGroupInGroupList' + ).mockReturnValue(false); initialize(); component['handleFocusLoopInMobileMode'](event); @@ -1616,10 +1608,13 @@ describe('ConfiguratorGroupMenuComponent', () => { const event = new KeyboardEvent('keydown', { code: 'Tab', }); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(true); - spyOn(configGroupMenuService, 'isActiveGroupInGroupList').and.returnValue( + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( true ); + vi.spyOn( + configGroupMenuService, + 'isActiveGroupInGroupList' + ).mockReturnValue(true); initialize(); component['handleFocusLoopInMobileMode'](event); @@ -1631,10 +1626,13 @@ describe('ConfiguratorGroupMenuComponent', () => { const event = new KeyboardEvent('keydown', { code: 'Tab', }); - spyOn(configGroupMenuService, 'isBackBtnFocused').and.returnValue(false); - spyOn(configGroupMenuService, 'isActiveGroupInGroupList').and.returnValue( - true + vi.spyOn(configGroupMenuService, 'isBackBtnFocused').mockReturnValue( + false ); + vi.spyOn( + configGroupMenuService, + 'isActiveGroupInGroupList' + ).mockReturnValue(true); initialize(); component['handleFocusLoopInMobileMode'](event); @@ -1663,10 +1661,11 @@ describe('ConfiguratorGroupMenuComponent', () => { describe('navigateUp', () => { it('should navigate up (and not set focus)', () => { - spyOn(configuratorGroupsService, 'getMenuParentGroup').and.returnValue( - of(mockProductConfiguration.groups[0]) - ); - spyOn(configuratorGroupsService, 'getParentGroup').and.returnValue( + vi.spyOn( + configuratorGroupsService, + 'getMenuParentGroup' + ).mockReturnValue(of(mockProductConfiguration.groups[0])); + vi.spyOn(configuratorGroupsService, 'getParentGroup').mockReturnValue( mockProductConfiguration.groups[0] ); component.navigateUp(); @@ -1677,10 +1676,11 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should navigate up and set focus if current group is provided', () => { - spyOn(configuratorGroupsService, 'getMenuParentGroup').and.returnValue( - of(mockProductConfiguration.groups[0]) - ); - spyOn(configuratorGroupsService, 'getParentGroup').and.returnValue( + vi.spyOn( + configuratorGroupsService, + 'getMenuParentGroup' + ).mockReturnValue(of(mockProductConfiguration.groups[0])); + vi.spyOn(configuratorGroupsService, 'getParentGroup').mockReturnValue( mockProductConfiguration.groups[0] ); @@ -1691,10 +1691,11 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should navigate up, parent group null', () => { - spyOn(configuratorGroupsService, 'getMenuParentGroup').and.returnValue( - of(mockProductConfiguration.groups[0]) - ); - spyOn(configuratorGroupsService, 'getParentGroup').and.callThrough(); + vi.spyOn( + configuratorGroupsService, + 'getMenuParentGroup' + ).mockReturnValue(of(mockProductConfiguration.groups[0])); + vi.spyOn(configuratorGroupsService, 'getParentGroup'); component.navigateUp(); expect(configuratorGroupsService.getParentGroup).toHaveBeenCalled(); @@ -1704,7 +1705,7 @@ describe('ConfiguratorGroupMenuComponent', () => { describe('getGroupMenuTitle', () => { it('should return only group description as title when expert mode is off', () => { - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(false) ); expect( @@ -1713,7 +1714,7 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should return group description and name as title when expert mode is on', () => { - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(true) ); const groupMenuTitle = @@ -1727,7 +1728,7 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should return only conflict header group description as title even if expert mode is on', () => { - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(true) ); const configForExpMode = productConfigurationWithConflicts; @@ -1738,7 +1739,7 @@ describe('ConfiguratorGroupMenuComponent', () => { }); it('should return only conflict group description as title even if expert mode is on', () => { - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(true) ); const configForExpMode = productConfigurationWithConflicts; diff --git a/feature-libs/product-configurator/rulebased/components/group-title/configurator-group-title.component.spec.ts b/feature-libs/product-configurator/rulebased/components/group-title/configurator-group-title.component.spec.ts index d26f1f81654..6c9317c9ee6 100644 --- a/feature-libs/product-configurator/rulebased/components/group-title/configurator-group-title.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/group-title/configurator-group-title.component.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { Router, RouterState } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -21,6 +21,7 @@ import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorGroupTitleComponent } from './configurator-group-title.component'; +import { vi } from 'vitest'; const config: Configurator.Configuration = ConfigurationTestData.productConfiguration; @@ -94,7 +95,7 @@ describe('ConfiguratorGroupTitleComponent', () => { let configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService; let hamburgerMenuService: HamburgerMenuService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { routerStateObservable = of(ConfigurationTestData.mockRouterState); TestBed.configureTestingModule({ imports: [ @@ -136,7 +137,7 @@ describe('ConfiguratorGroupTitleComponent', () => { add: { imports: [MockHamburgerMenuComponent] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorGroupTitleComponent); component = fixture.componentInstance; @@ -147,32 +148,37 @@ describe('ConfiguratorGroupTitleComponent', () => { configuratorUtils = TestBed.inject(CommonConfiguratorUtilsService); configuratorUtils.setOwnerKey(config.owner); - spyOn(configuratorGroupsService, 'navigateToGroup').and.stub(); + vi.spyOn(configuratorGroupsService, 'navigateToGroup').mockImplementation( + () => {} + ); configExpertModeService = TestBed.inject(ConfiguratorExpertModeService); breakpointService = TestBed.inject(BreakpointService); - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService ); - spyOn(configuratorStorefrontUtilsService, 'changeStyling').and.stub(); - spyOn(configuratorStorefrontUtilsService, 'removeStyling'); - spyOn( + vi.spyOn( + configuratorStorefrontUtilsService, + 'changeStyling' + ).mockImplementation(() => {}); + vi.spyOn(configuratorStorefrontUtilsService, 'removeStyling'); + vi.spyOn( configuratorStorefrontUtilsService, 'focusFirstActiveElement' - ).and.stub(); + ).mockImplementation(() => {}); hamburgerMenuService = TestBed.inject(HamburgerMenuService); - spyOn(hamburgerMenuService, 'toggle').and.callThrough(); + vi.spyOn(hamburgerMenuService, 'toggle'); }); it('should create component with expanded hamburger menu icon', () => { hamburgerMenuService.toggle(false); - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); fixture.detectChanges(); expect(component).toBeDefined(); expect( @@ -194,7 +200,7 @@ describe('ConfiguratorGroupTitleComponent', () => { }); it('should create component with hamburger menu icon', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); fixture.detectChanges(); expect(component).toBeDefined(); CommonConfiguratorTestUtilsService.expectElementPresent( @@ -228,7 +234,7 @@ describe('ConfiguratorGroupTitleComponent', () => { describe('getGroupTitle', () => { it('should return group title', () => { - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(false) ); expect(component.getGroupTitle(config.groups[0])).toEqual( @@ -237,7 +243,7 @@ describe('ConfiguratorGroupTitleComponent', () => { }); it('should return group title for expert mode', () => { - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(true) ); const groupMenuTitle = @@ -248,7 +254,7 @@ describe('ConfiguratorGroupTitleComponent', () => { it('should return conflict group title for expert mode', () => { const configForExpMode = ConfigurationTestData.productConfigurationWithConflicts; - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(true) ); fixture.detectChanges(); @@ -260,7 +266,7 @@ describe('ConfiguratorGroupTitleComponent', () => { describe('isMobile', () => { it('should not render hamburger menu in desktop mode', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(false)); fixture.detectChanges(); component.isMobile().subscribe((isMobile) => { @@ -274,7 +280,7 @@ describe('ConfiguratorGroupTitleComponent', () => { }); it('should render hamburger menu in mobile mode', () => { - spyOn(breakpointService, 'isDown').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isDown').mockReturnValue(of(true)); fixture.detectChanges(); component.isMobile().subscribe((isMobile) => { @@ -290,7 +296,7 @@ describe('ConfiguratorGroupTitleComponent', () => { describe('ngOnDestroy', () => { it('should unsubscribe and remove styling on ngOnDestroy', () => { - const spyUnsubscribe = spyOn(Subscription.prototype, 'unsubscribe'); + const spyUnsubscribe = vi.spyOn(Subscription.prototype, 'unsubscribe'); component.ngOnDestroy(); expect(spyUnsubscribe).toHaveBeenCalled(); expect( diff --git a/feature-libs/product-configurator/rulebased/components/group/configurator-group.component.spec.ts b/feature-libs/product-configurator/rulebased/components/group/configurator-group.component.spec.ts index 0ce7ef933ff..09739ddb772 100644 --- a/feature-libs/product-configurator/rulebased/components/group/configurator-group.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/group/configurator-group.component.spec.ts @@ -7,7 +7,7 @@ import { Output, Type, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -56,6 +56,7 @@ import { ConfigFormUpdateEvent } from '../form/configurator-form.event'; import { ConfiguratorPriceComponentOptions } from '../price/configurator-price.component'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorGroupComponent } from './configurator-group.component'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; @@ -297,10 +298,10 @@ describe('ConfiguratorGroupComponent', () => { let fixture: ComponentFixture; let component: ConfiguratorGroupComponent; - beforeEach(waitForAsync(() => { + beforeEach(async () => { mockLanguageService = { getAll: () => of([]), - getActive: jasmine.createSpy().and.returnValue(of('en')), + getActive: vi.fn().mockReturnValue(of('en')), }; TestBed.configureTestingModule({ @@ -371,7 +372,7 @@ describe('ConfiguratorGroupComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { configuratorUtils = TestBed.inject( @@ -383,17 +384,14 @@ describe('ConfiguratorGroupComponent', () => { configuratorGroupsService = TestBed.inject( ConfiguratorGroupsService as Type ); - spyOn( - configuratorCommonsService, - 'isConfigurationLoading' - ).and.callThrough(); - spyOn(configuratorGroupsService, 'setGroupStatusVisited').and.callThrough(); + vi.spyOn(configuratorCommonsService, 'isConfigurationLoading'); + vi.spyOn(configuratorGroupsService, 'setGroupStatusVisited'); configExpertModeService = TestBed.inject( ConfiguratorExpertModeService as Type ); - spyOn(configExpertModeService, 'setExpModeRequested').and.callThrough(); - spyOn(configExpertModeService, 'setExpModeActive').and.callThrough(); + vi.spyOn(configExpertModeService, 'setExpModeRequested'); + vi.spyOn(configExpertModeService, 'setExpModeActive'); configuratorUtils.setOwnerKey(OWNER); storefrontUtils = TestBed.inject( @@ -419,9 +417,10 @@ describe('ConfiguratorGroupComponent', () => { }); it('should display conflict description and suggestions for a conflict group', () => { - spyOn(configuratorGroupsService, 'isConflictGroupType').and.returnValue( - true - ); + vi.spyOn( + configuratorGroupsService, + 'isConflictGroupType' + ).mockReturnValue(true); const component = createComponent(); component.group = ConfigurationTestData.productConfigurationWithConflicts.groups[0].subGroups[0]; @@ -631,7 +630,7 @@ describe('ConfiguratorGroupComponent', () => { describe('isConflictGroupType', () => { it('should not call configurator group service to check group type', () => { - spyOn(configuratorGroupsService, 'isConflictGroupType').and.callThrough(); + vi.spyOn(configuratorGroupsService, 'isConflictGroupType'); createComponent().isConflictGroupType(undefined); expect( configuratorGroupsService.isConflictGroupType @@ -639,7 +638,7 @@ describe('ConfiguratorGroupComponent', () => { }); it('should call configurator group service to check group type', () => { - spyOn(configuratorGroupsService, 'isConflictGroupType').and.callThrough(); + vi.spyOn(configuratorGroupsService, 'isConflictGroupType'); createComponent().isConflictGroupType( Configurator.GroupType.CONFLICT_GROUP ); @@ -650,7 +649,7 @@ describe('ConfiguratorGroupComponent', () => { }); it('should update a configuration through the facade layer ', () => { - spyOn(configuratorCommonsService, 'updateConfiguration').and.callThrough(); + vi.spyOn(configuratorCommonsService, 'updateConfiguration'); isConfigurationLoadingObservable = cold('xy', { x: true, y: false, @@ -666,18 +665,20 @@ describe('ConfiguratorGroupComponent', () => { describe('displayConflictDescription', () => { it('should return true if group is conflict group and has a name', () => { - spyOn(configuratorGroupsService, 'isConflictGroupType').and.returnValue( - true - ); + vi.spyOn( + configuratorGroupsService, + 'isConflictGroupType' + ).mockReturnValue(true); expect(createComponent().displayConflictDescription(conflictGroup)).toBe( true ); }); it('should return false if group is standard group', () => { - spyOn(configuratorGroupsService, 'isConflictGroupType').and.returnValue( - false - ); + vi.spyOn( + configuratorGroupsService, + 'isConflictGroupType' + ).mockReturnValue(false); expect(createComponent().displayConflictDescription(conflictGroup)).toBe( false ); @@ -690,9 +691,10 @@ describe('ConfiguratorGroupComponent', () => { }); it('should return false if group is conflict group and does not have a name', () => { - spyOn(configuratorGroupsService, 'isConflictGroupType').and.returnValue( - true - ); + vi.spyOn( + configuratorGroupsService, + 'isConflictGroupType' + ).mockReturnValue(true); conflictGroup.name = ''; expect(createComponent().displayConflictDescription(conflictGroup)).toBe( false @@ -729,7 +731,7 @@ describe('ConfiguratorGroupComponent', () => { describe('createAttributeUiKey', () => { it('should call method of configuratoreStorefrontUtils', () => { - spyOn(storefrontUtils, 'createAttributeUiKey').and.callThrough(); + vi.spyOn(storefrontUtils, 'createAttributeUiKey'); createComponent().createAttributeUiKey('prefix', 'attributeId'); expect(storefrontUtils.createAttributeUiKey).toHaveBeenCalledWith( 'prefix', @@ -741,7 +743,7 @@ describe('ConfiguratorGroupComponent', () => { describe('with regards to expMode', () => { it("should check whether expert mode status is set to 'true'", () => { createComponent(); - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(true) ); @@ -756,7 +758,7 @@ describe('ConfiguratorGroupComponent', () => { it("should check whether expert mode status is set to 'false'", () => { createComponent(); - spyOn(configExpertModeService, 'getExpModeActive').and.returnValue( + vi.spyOn(configExpertModeService, 'getExpModeActive').mockReturnValue( of(false) ); diff --git a/feature-libs/product-configurator/rulebased/components/overview-attribute/configurator-overview-attribute.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-attribute/configurator-overview-attribute.component.spec.ts index 3ff5981fe70..65838a301cd 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-attribute/configurator-overview-attribute.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-attribute/configurator-overview-attribute.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { I18nTestingModule } from '@spartacus/core'; @@ -11,6 +11,7 @@ import { ConfiguratorPriceComponentOptions, } from '../price/configurator-price.component'; import { ConfiguratorOverviewAttributeComponent } from './configurator-overview-attribute.component'; +import { vi } from 'vitest'; @Component({ selector: 'cx-configurator-price', @@ -27,7 +28,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { let htmlElem: HTMLElement; let breakpointService: BreakpointService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -43,7 +44,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { imports: [MockConfiguratorPriceComponent], }, }); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorOverviewAttributeComponent); component = fixture.componentInstance; @@ -52,7 +53,6 @@ describe('ConfigurationOverviewAttributeComponent', () => { attribute: 'Test Attribute Name', value: 'Test Attribute Value', }; - fixture.detectChanges(); breakpointService = TestBed.inject( BreakpointService as Type @@ -60,10 +60,12 @@ describe('ConfigurationOverviewAttributeComponent', () => { }); it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should show attribute value', () => { + fixture.detectChanges(); expect(htmlElem.querySelectorAll('.cx-attribute-value').length).toBe(1); expect(htmlElem.querySelectorAll('.cx-attribute-label').length).toBe(1); @@ -99,7 +101,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { describe('isDesktop', () => { it('should return `false` because we deal with mobile widget', () => { - spyOn(breakpointService, 'isUp').and.returnValue(of(false)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(false)); let result: boolean; component .isDesktop() @@ -111,7 +113,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { }); it('should return `true` because we deal with desktop widget', () => { - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); let result: boolean; component .isDesktop() @@ -138,12 +140,11 @@ describe('ConfigurationOverviewAttributeComponent', () => { value: 10, }, }; - fixture.detectChanges(); breakpointService = TestBed.inject( BreakpointService as Type ); - spyOn(breakpointService, 'isUp').and.returnValue(of(true)); + vi.spyOn(breakpointService, 'isUp').mockReturnValue(of(true)); }); it("should contain span element with class name 'cx-visually-hidden' without price that hides span element content on the UI", () => { @@ -175,6 +176,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { }); it("should contain span element with class name 'cx-visually-hidden' with price that hides span element content on the UI", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -193,6 +195,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { }); it("should contain div element with class name 'cx-attribute-value' and 'aria-hidden' attribute that removes div element from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -205,6 +208,7 @@ describe('ConfigurationOverviewAttributeComponent', () => { }); it("should contain div element with class name 'cx-attribute-label' and 'aria-hidden' attribute that removes div element from the accessibility tree", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/overview-bundle-attribute/configurator-overview-bundle-attribute.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-bundle-attribute/configurator-overview-bundle-attribute.component.spec.ts index 19648ea9706..ba76e8bcc37 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-bundle-attribute/configurator-overview-bundle-attribute.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-bundle-attribute/configurator-overview-bundle-attribute.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CxNumericPipe, @@ -9,7 +9,7 @@ import { ProductService, } from '@spartacus/core'; import { MediaModule } from '@spartacus/storefront'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { Configurator } from '../../core/model/configurator.model'; @@ -68,7 +68,7 @@ describe('ConfiguratorOverviewBundleAttributeComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [MediaModule, ConfiguratorOverviewBundleAttributeComponent], providers: [{ provide: ProductService, useClass: MockProductService }], @@ -82,7 +82,7 @@ describe('ConfiguratorOverviewBundleAttributeComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -102,28 +102,22 @@ describe('ConfiguratorOverviewBundleAttributeComponent', () => { }); describe('product', () => { - it('should use dummy product if no product code exists', (done: DoneFn) => { + it('should use dummy product if no product code exists', async () => { product$.next(noCommerceProduct); fixture.detectChanges(); - component.product$.pipe(take(1)).subscribe((product: Product) => { - expect(product).toEqual(noCommerceProduct); - - done(); - }); + const product = await firstValueFrom(component.product$); + expect(product).toEqual(noCommerceProduct); }); - it('should exist with product code', (done: DoneFn) => { + it('should exist with product code', async () => { product$.next(mockProduct); fixture.detectChanges(); - component.product$.pipe(take(1)).subscribe((product: Product) => { - expect(product).toEqual(mockProduct); - - done(); - }); + const product = await firstValueFrom(component.product$); + expect(product).toEqual(mockProduct); }); }); diff --git a/feature-libs/product-configurator/rulebased/components/overview-filter-bar/configurator-overview-filter-bar.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-filter-bar/configurator-overview-filter-bar.component.spec.ts index 83744ccf344..0e6a9f383f2 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-filter-bar/configurator-overview-filter-bar.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-filter-bar/configurator-overview-filter-bar.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CxDatePipe, @@ -16,6 +16,7 @@ import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorOverviewFilterBarComponent } from './configurator-overview-filter-bar.component'; +import { vi } from 'vitest'; const owner: CommonConfigurator.Owner = ConfigurationTestData.productConfiguration.owner; @@ -56,9 +57,7 @@ function initTestData() { }; } function initMocks() { - mockConfigCommonsService = jasmine.createSpyObj([ - 'updateConfigurationOverview', - ]); + mockConfigCommonsService = { updateConfigurationOverview: vi.fn() } as any; } @Component({ @@ -73,7 +72,7 @@ class MockConfigUtilsService { } describe('ConfiguratorOverviewFilterBarComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { initTestData(); initMocks(); TestBed.configureTestingModule({ @@ -96,20 +95,20 @@ describe('ConfiguratorOverviewFilterBarComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorOverviewFilterBarComponent); htmlElem = fixture.nativeElement; component = fixture.componentInstance; component.config = ovConfig; - fixture.detectChanges(); configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService as Type ); }); describe('in a component test environment', () => { it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); @@ -195,7 +194,7 @@ describe('ConfiguratorOverviewFilterBarComponent', () => { let buttonEl = fixture.debugElement.query( By.css('#cx-overview-filter-applied-USER_INPUT') ); - spyOn(component, 'onAttrFilterRemove'); + vi.spyOn(component, 'onAttrFilterRemove'); const event = new KeyboardEvent('keydown', { key: 'Delete', @@ -426,10 +425,7 @@ describe('ConfiguratorOverviewFilterBarComponent', () => { describe('focusElementById', () => { it('should call getElement method of ConfiguratorStorefrontUtilsService using # as prefix', () => { - spyOn( - configuratorStorefrontUtilsService, - 'getElement' - ).and.callThrough(); + vi.spyOn(configuratorStorefrontUtilsService, 'getElement'); component['focusElementById'](FIRST_FILTER_CHECKBOX_ID); expect( configuratorStorefrontUtilsService.getElement @@ -437,19 +433,21 @@ describe('ConfiguratorOverviewFilterBarComponent', () => { }); it('should call focus method of html element', () => { - let mockElement = jasmine.createSpyObj('HTMLElement', ['focus']); - spyOn(configuratorStorefrontUtilsService, 'getElement').and.returnValue( - mockElement - ); + let mockElement = { focus: vi.fn() }; + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElement' + ).mockReturnValue(mockElement); component['focusElementById'](FIRST_FILTER_CHECKBOX_ID); expect(mockElement.focus).toHaveBeenCalled(); }); it('should not call focus method if getElement returns null', () => { - let mockElement = jasmine.createSpyObj('HTMLElement', ['focus']); - spyOn(configuratorStorefrontUtilsService, 'getElement').and.returnValue( - null - ); + let mockElement = { focus: vi.fn() }; + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElement' + ).mockReturnValue(null); component['focusElementById'](FIRST_FILTER_CHECKBOX_ID); expect(mockElement.focus).not.toHaveBeenCalled(); }); diff --git a/feature-libs/product-configurator/rulebased/components/overview-filter-button/configurator-overview-filter-button.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-filter-button/configurator-overview-filter-button.component.spec.ts index 894086adf03..0b0d330f47b 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-filter-button/configurator-overview-filter-button.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-filter-button/configurator-overview-filter-button.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; import { @@ -9,7 +9,6 @@ import { import { ConfiguratorStorefrontUtilsService } from '@spartacus/product-configurator/rulebased'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { EMPTY, NEVER, Observable, of } from 'rxjs'; -import { delay } from 'rxjs/operators'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorCommonsService } from '../../core'; import { Configurator } from '../../core/model/configurator.model'; @@ -17,6 +16,7 @@ import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorOverviewFilterButtonComponent } from './configurator-overview-filter-button.component'; import { ConfiguratorOverviewFilterBarComponent } from '../overview-filter-bar/configurator-overview-filter-bar.component'; +import { vi } from 'vitest'; const owner: CommonConfigurator.Owner = ConfigurationTestData.productConfiguration.owner; @@ -35,7 +35,7 @@ let mockConfigCommonsService: ConfiguratorCommonsService; let ovConfig: Configurator.ConfigurationWithOverview; function asSpy(f: any) { - return f; + return f; } function initTestData() { @@ -53,20 +53,21 @@ function initComponent() { htmlElem = fixture.nativeElement; component = fixture.componentInstance; isDisplayOnlyVariant = false; - fixture.detectChanges(); } function initMocks() { - mockLaunchDialogService = jasmine.createSpyObj(['openDialogAndSubscribe']); - mockConfigRouterService = jasmine.createSpyObj(['extractRouterData']); - mockConfigCommonsService = jasmine.createSpyObj(['getConfiguration']); - asSpy(mockConfigRouterService.extractRouterData).and.returnValue( + mockLaunchDialogService = { openDialogAndSubscribe: vi.fn() } as any; + mockConfigRouterService = { extractRouterData: vi.fn() } as any; + mockConfigCommonsService = { getConfiguration: vi.fn() } as any; + (mockConfigRouterService.extractRouterData as any).mockReturnValue( of(ConfigurationTestData.mockRouterState) ); - asSpy(mockConfigCommonsService.getConfiguration).and.returnValue( - of(ovConfig).pipe(delay(0)) // delay(0) to avoid NG0100 error in test + (mockConfigCommonsService.getConfiguration as any).mockReturnValue( + of(ovConfig) + ); + (mockLaunchDialogService.openDialogAndSubscribe as any).mockReturnValue( + EMPTY ); - asSpy(mockLaunchDialogService.openDialogAndSubscribe).and.returnValue(EMPTY); } @Component({ @@ -86,7 +87,7 @@ class MockConfiguratorStorefrontUtilsService { } describe('ConfigurationOverviewFilterButtonComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { initTestData(); initMocks(); TestBed.configureTestingModule({ @@ -120,17 +121,18 @@ describe('ConfigurationOverviewFilterButtonComponent', () => { }) .compileComponents(); initComponent(); - })); - - beforeEach(() => { - fixture.detectChanges(); //due to the additional delay(0) + // Pre-set ghostStyle to avoid NG0100 from tap() side-effect during first detectChanges. + // The 'while loading' test re-initializes with NEVER observable so ghostStyle stays true. + component.ghostStyle = false; }); it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should open filter modal on request', () => { + fixture.detectChanges(); fixture.debugElement .query(By.css('.cx-config-filter-button')) .triggerEventHandler('click'); @@ -142,6 +144,7 @@ describe('ConfigurationOverviewFilterButtonComponent', () => { }); it('should render filter button', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -186,6 +189,7 @@ describe('ConfigurationOverviewFilterButtonComponent', () => { }); it('should render filter button without count if there are no active filters', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -195,8 +199,9 @@ describe('ConfigurationOverviewFilterButtonComponent', () => { }); it('while loading should not render filter button but ghost button instead', () => { - asSpy(mockConfigCommonsService.getConfiguration).and.returnValue(NEVER); + asSpy(mockConfigCommonsService.getConfiguration).mockReturnValue(NEVER); initComponent(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -214,6 +219,7 @@ describe('ConfigurationOverviewFilterButtonComponent', () => { describe('to support A11Y', () => { it('filter button should have descriptive title', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/overview-filter-dialog/configurator-overview-filter-dialog.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-filter-dialog/configurator-overview-filter-dialog.component.spec.ts index e515a6022f6..dd667e57752 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-filter-dialog/configurator-overview-filter-dialog.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-filter-dialog/configurator-overview-filter-dialog.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Directive, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CxDatePipe, @@ -22,6 +22,7 @@ import { Configurator } from '../../core/model/configurator.model'; import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorOverviewFilterComponent } from '../overview-filter/configurator-overview-filter.component'; import { ConfiguratorOverviewFilterDialogComponent } from './configurator-overview-filter-dialog.component'; +import { vi } from 'vitest'; let component: ConfiguratorOverviewFilterDialogComponent; let fixture: ComponentFixture; @@ -40,7 +41,7 @@ function initialize() { function initializeMocks() { mockLaunchDialogService = { - closeDialog: jasmine.createSpy(), + closeDialog: vi.fn(), data$: of(ovConfig), }; } @@ -67,7 +68,7 @@ export class MockKeyboadFocusDirective { } describe('ConfiguratorOverviewFilterDialogComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { initializeMocks(); TestBed.configureTestingModule({ imports: [ConfiguratorOverviewFilterDialogComponent], @@ -98,7 +99,7 @@ describe('ConfiguratorOverviewFilterDialogComponent', () => { }, }) .compileComponents(); - })); + }); it('should create component', () => { initialize(); diff --git a/feature-libs/product-configurator/rulebased/components/overview-filter/configurator-overview-filter.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-filter/configurator-overview-filter.component.spec.ts index 672f4159c61..2b2fefb26e6 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-filter/configurator-overview-filter.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-filter/configurator-overview-filter.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; @@ -13,6 +13,7 @@ import { Configurator } from '../../core/model/configurator.model'; import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorOverviewFilterComponent } from './configurator-overview-filter.component'; +import { vi } from 'vitest'; const owner: CommonConfigurator.Owner = ConfigurationTestData.productConfiguration.owner; @@ -46,9 +47,7 @@ function initTestData() { } function initMocks() { - mockConfigCommonsService = jasmine.createSpyObj([ - 'updateConfigurationOverview', - ]); + mockConfigCommonsService = { updateConfigurationOverview: vi.fn() } as any; } function initTestComponent() { @@ -58,7 +57,6 @@ function initTestComponent() { component.config = overview; isDisplayOnlyVariant = false; component.ngOnChanges(); - fixture.detectChanges(); } @Component({ @@ -106,19 +104,21 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); } - beforeEach(waitForAsync(() => { + beforeEach(async () => { initTestData(); initMocks(); configureTestingModule().compileComponents(); initTestComponent(); - })); + }); describe('in a component test environment', () => { it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should render filter options', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectNumberOfElementsPresent( expect, htmlElem, @@ -128,6 +128,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('should render both filter headers', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectNumberOfElementsPresent( expect, htmlElem, @@ -190,6 +191,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('should render filter bar by default', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -208,6 +210,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('should update overview on change of filter option', () => { + fixture.detectChanges(); fixture.debugElement .queryAll(By.css('.cx-overview-filter-option input')) .forEach((element) => { @@ -221,6 +224,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { describe('to support A11Y', () => { it('price filter label should be linked to checkbox', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, @@ -231,6 +235,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('price filter label should have a11y text', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, @@ -241,6 +246,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('my selections filter label should be linked to checkbox', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, @@ -252,6 +258,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('my selections filter label should have a11y text', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, @@ -262,6 +269,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('group filter label should be linked to checkbox', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, @@ -273,6 +281,7 @@ describe('ConfiguratorOverviewFilterComponent', () => { }); it('group filter label should have a11y text', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementToHaveAttributeWithValue( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/overview-form/configurator-overview-form.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-form/configurator-overview-form.component.spec.ts index 2b05da203b5..059102cfc71 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-form/configurator-overview-form.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-form/configurator-overview-form.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Injectable, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterState } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -158,7 +158,7 @@ class MockDirectionService implements Partial { } describe('ConfigurationOverviewFormComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -192,7 +192,7 @@ describe('ConfigurationOverviewFormComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { routerStateObservable = null; configurationObservable = null; diff --git a/feature-libs/product-configurator/rulebased/components/overview-menu/configurator-overview-menu.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-menu/configurator-overview-menu.component.spec.ts index 9017d871f27..0d523118668 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-menu/configurator-overview-menu.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-menu/configurator-overview-menu.component.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; @@ -11,6 +11,7 @@ import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorOverviewMenuComponent } from './configurator-overview-menu.component'; +import { vi } from 'vitest'; const OWNER: CommonConfigurator.Owner = ConfigurationTestData.productConfiguration.owner; @@ -65,41 +66,34 @@ function initialize() { htmlElem = fixture.nativeElement; component = fixture.componentInstance; component.config = CONFIGURATION; - fixture.detectChanges(); configuratorGroupsService = TestBed.inject( ConfiguratorGroupsService as Type ); - spyOn(configuratorGroupsService, 'setGroupStatusVisited').and.callThrough(); + vi.spyOn(configuratorGroupsService, 'setGroupStatusVisited'); configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService as Type ); - spyOn(configuratorStorefrontUtilsService, 'scrollToConfigurationElement'); + vi.spyOn(configuratorStorefrontUtilsService, 'scrollToConfigurationElement'); - spyOn(configuratorStorefrontUtilsService, 'ensureElementVisible'); + vi.spyOn(configuratorStorefrontUtilsService, 'ensureElementVisible'); - spyOn(configuratorStorefrontUtilsService, 'changeStyling'); + vi.spyOn(configuratorStorefrontUtilsService, 'changeStyling'); - spyOn(configuratorStorefrontUtilsService, 'removeStyling'); + vi.spyOn(configuratorStorefrontUtilsService, 'removeStyling'); - spyOn( - configuratorStorefrontUtilsService, - 'createOvGroupId' - ).and.callThrough(); + vi.spyOn(configuratorStorefrontUtilsService, 'createOvGroupId'); - spyOn( - configuratorStorefrontUtilsService, - 'createOvMenuItemId' - ).and.callThrough(); + vi.spyOn(configuratorStorefrontUtilsService, 'createOvMenuItemId'); - spyOn(configuratorStorefrontUtilsService, 'getPrefixId').and.callThrough(); + vi.spyOn(configuratorStorefrontUtilsService, 'getPrefixId'); } describe('ConfigurationOverviewMenuComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -122,23 +116,29 @@ describe('ConfigurationOverviewMenuComponent', () => { add: { imports: [MockTranslatePipe, MockIconComponent] }, }) .compileComponents(); - })); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); it('should create component', () => { initialize(); + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should call ngAfterViewInit after ovMenu is rendered', () => { initialize(); - spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); - spyOn(configuratorStorefrontUtilsService, 'getElement'); - spyOn(configuratorStorefrontUtilsService, 'getElements'); - spyOn( + fixture.detectChanges(); + vi.spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); + vi.spyOn(configuratorStorefrontUtilsService, 'getElement'); + vi.spyOn(configuratorStorefrontUtilsService, 'getElements'); + vi.spyOn( configuratorStorefrontUtilsService, 'getVerticallyScrolledPixels' - ).and.returnValue(0); - spyOn(configuratorStorefrontUtilsService, 'hasScrollbar'); + ).mockReturnValue(0); + vi.spyOn(configuratorStorefrontUtilsService, 'hasScrollbar'); component.ngAfterViewInit(); fixture.detectChanges(); @@ -163,6 +163,7 @@ describe('ConfigurationOverviewMenuComponent', () => { it('should provide the overview groups', () => { initialize(); + fixture.detectChanges(); expect(component.config.overview?.groups?.length).toBe(2); }); @@ -189,14 +190,14 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should return zero because amount is zero', () => { - component.amount = 0; fixture.detectChanges(); + component.amount = 0; expect(component['getMenuItemsHeight']()).toEqual(0); }); it('should return the total height of all menu items', () => { - component.amount = 10; fixture.detectChanges(); + component.amount = 10; expect(component['getMenuItemsHeight']()).toEqual(395); }); }); @@ -207,6 +208,8 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should call changeStyling', () => { + fixture.detectChanges(); + vi.clearAllMocks(); component['changeStyling'](); expect( configuratorStorefrontUtilsService.changeStyling @@ -220,6 +223,8 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should call removeStyling', () => { + fixture.detectChanges(); + vi.clearAllMocks(); component['removeStyling'](); expect( configuratorStorefrontUtilsService.removeStyling @@ -233,8 +238,9 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should change styling', () => { - component.amount = 1; fixture.detectChanges(); + vi.clearAllMocks(); + component.amount = 1; component['adjustStyling'](); expect( configuratorStorefrontUtilsService.changeStyling @@ -242,8 +248,9 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should removeStyling styling', () => { - component.amount = 0; fixture.detectChanges(); + vi.clearAllMocks(); + component.amount = 0; component['adjustStyling'](); expect( configuratorStorefrontUtilsService.removeStyling @@ -253,6 +260,7 @@ describe('ConfigurationOverviewMenuComponent', () => { it('should render group descriptions', () => { initialize(); + fixture.detectChanges(); expect(htmlElem.innerHTML).toContain( ConfigurationTestData.OV_GROUP_DESCRIPTION ); @@ -261,6 +269,7 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('getGroupLevelStyleClasses', () => { it('should return style class according to level', () => { initialize(); + fixture.detectChanges(); const styleClass = component.getGroupLevelStyleClasses(4); expect(styleClass).toBe('cx-menu-group groupLevel4'); }); @@ -269,6 +278,7 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('navigateToGroup', () => { it('should invoke utils service for determining group id', () => { initialize(); + fixture.detectChanges(); component.navigateToGroup(GROUP_PREFIX, GROUP_ID_LOCAL); expect( configuratorStorefrontUtilsService.createOvGroupId @@ -277,6 +287,7 @@ describe('ConfigurationOverviewMenuComponent', () => { it('should invoke utils service for scrolling', () => { initialize(); + fixture.detectChanges(); component.navigateToGroup(GROUP_PREFIX, GROUP_ID_LOCAL); expect( configuratorStorefrontUtilsService.scrollToConfigurationElement @@ -287,6 +298,7 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('getPrefixId', () => { it('should call configuratorStorefrontUtilsService.getPrefixId method', () => { initialize(); + fixture.detectChanges(); component.getPrefixId('AAA', 'BBB'); expect( configuratorStorefrontUtilsService.getPrefixId @@ -297,6 +309,7 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('getGroupId', () => { it('should dispatch request to utils service', () => { initialize(); + fixture.detectChanges(); component.getGroupId('A', 'B'); expect( configuratorStorefrontUtilsService.createOvGroupId @@ -307,6 +320,7 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('getMenuItemId', () => { it('should dispatch request to utils service', () => { initialize(); + fixture.detectChanges(); component.getMenuItemId('A', 'B'); expect( configuratorStorefrontUtilsService.createOvMenuItemId @@ -317,11 +331,13 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('onScroll', () => { beforeEach(() => { initialize(); - spyOn(configuratorStorefrontUtilsService, 'getElements'); - spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); + vi.spyOn(configuratorStorefrontUtilsService, 'getElements'); + vi.spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); }); it('should call onScroll method', () => { + fixture.detectChanges(); + vi.clearAllMocks(); component.onScroll(); expect( @@ -337,10 +353,12 @@ describe('ConfigurationOverviewMenuComponent', () => { describe('onResize', () => { beforeEach(() => { initialize(); - spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); + vi.spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); }); it('should call onResize method', () => { + fixture.detectChanges(); + vi.clearAllMocks(); component.onResize(); expect( @@ -361,20 +379,20 @@ describe('ConfigurationOverviewMenuComponent', () => { it('should return empty string because spare viewport height is larger that menu items height', () => { component.menuItemsHeight = 400; fixture.detectChanges(); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'getSpareViewportHeight' - ).and.returnValue(600); + ).mockReturnValue(600); expect(component['getHeight']()).toEqual(''); }); it('should return spare viewport height because menu items height is equal zero', () => { component.menuItemsHeight = 400; fixture.detectChanges(); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'getSpareViewportHeight' - ).and.returnValue(200); + ).mockReturnValue(200); expect(component['getHeight']()).toEqual('200px'); }); }); @@ -414,9 +432,10 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should not get menu item to highlight because getElements method return undefined', () => { - spyOn(configuratorStorefrontUtilsService, 'getElements').and.returnValue( - undefined - ); + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElements' + ).mockReturnValue(undefined); fixture.detectChanges(); expect(component['getMenuItemToHighlight']()).not.toBeDefined(); @@ -425,15 +444,16 @@ describe('ConfigurationOverviewMenuComponent', () => { it('should not get menu item to highlight because getScrollY method return undefined', () => { groups = createElements('div'); - spyOn(document, 'querySelectorAll').and.returnValue(groups); - spyOn(configuratorStorefrontUtilsService, 'getElements').and.returnValue( - groups - ); + vi.spyOn(document, 'querySelectorAll').mockReturnValue(groups); + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElements' + ).mockReturnValue(groups); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'getVerticallyScrolledPixels' - ).and.returnValue(undefined); + ).mockReturnValue(undefined); fixture.detectChanges(); @@ -443,23 +463,25 @@ describe('ConfigurationOverviewMenuComponent', () => { it('should get menu item to highlight', () => { groups = createElements('div'); - spyOn(document, 'querySelectorAll').and.returnValue(groups); - spyOn(configuratorStorefrontUtilsService, 'getElements').and.returnValue( - groups - ); + vi.spyOn(document, 'querySelectorAll').mockReturnValue(groups); + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElements' + ).mockReturnValue(groups); - spyOn( + vi.spyOn( configuratorStorefrontUtilsService, 'getVerticallyScrolledPixels' - ).and.returnValue(123); + ).mockReturnValue(123); + + fixture.detectChanges(); let menuItems = htmlElem.querySelectorAll('.cx-menu-item'); let menuItem = menuItems[menuItems.length - 1] as HTMLElement; - spyOn(configuratorStorefrontUtilsService, 'getElement').and.returnValue( - menuItem - ); - - fixture.detectChanges(); + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElement' + ).mockReturnValue(menuItem); expect(component['getMenuItemToHighlight']()?.id).toEqual(menuItem.id); }); @@ -471,13 +493,15 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should not highlight any element because the list of menu items is empty', () => { + fixture.detectChanges(); const menuItems: HTMLElement[] = Array.from( htmlElem.querySelectorAll('button.cx-menu-item') ); const elementToHighlight = menuItems[menuItems.length - 1]; - spyOn(configuratorStorefrontUtilsService, 'getElements').and.returnValue( - undefined - ); + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElements' + ).mockReturnValue(undefined); component['highlight'](elementToHighlight); expect( elementToHighlight.classList.contains(component['ACTIVE_CLASS']) @@ -485,13 +509,15 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should highlight an element', () => { + fixture.detectChanges(); const menuItems: HTMLElement[] = Array.from( htmlElem.querySelectorAll('button.cx-menu-item') ); const elementToHighlight = menuItems[menuItems.length - 1]; - spyOn(configuratorStorefrontUtilsService, 'getElements').and.returnValue( - menuItems - ); + vi.spyOn( + configuratorStorefrontUtilsService, + 'getElements' + ).mockReturnValue(menuItems); component['highlight'](elementToHighlight); expect( elementToHighlight.classList.contains(component['ACTIVE_CLASS']) @@ -505,7 +531,8 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should not call ensureElementVisible method because elementToHighlight is undefined', () => { - spyOn(configuratorStorefrontUtilsService, 'hasScrollbar'); + fixture.detectChanges(); + vi.spyOn(configuratorStorefrontUtilsService, 'hasScrollbar'); component['ensureElementVisible'](undefined); expect( configuratorStorefrontUtilsService.hasScrollbar @@ -516,13 +543,15 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should not call ensureElementVisible method because isScrollBox is false', () => { + fixture.detectChanges(); const menuItems: HTMLElement[] = Array.from( htmlElem.querySelectorAll('button.cx-menu-item') ); const element = menuItems[menuItems.length - 1]; - spyOn(configuratorStorefrontUtilsService, 'hasScrollbar').and.returnValue( - false - ); + vi.spyOn( + configuratorStorefrontUtilsService, + 'hasScrollbar' + ).mockReturnValue(false); component['ensureElementVisible'](element); expect( configuratorStorefrontUtilsService.hasScrollbar @@ -533,13 +562,15 @@ describe('ConfigurationOverviewMenuComponent', () => { }); it('should ensure visibility of an element', () => { + fixture.detectChanges(); const menuItems: HTMLElement[] = Array.from( htmlElem.querySelectorAll('button.cx-menu-item') ); const element = menuItems[menuItems.length - 1]; - spyOn(configuratorStorefrontUtilsService, 'hasScrollbar').and.returnValue( - true - ); + vi.spyOn( + configuratorStorefrontUtilsService, + 'hasScrollbar' + ).mockReturnValue(true); component['ensureElementVisible'](element); expect( configuratorStorefrontUtilsService.hasScrollbar diff --git a/feature-libs/product-configurator/rulebased/components/overview-notification-banner/configurator-overview-notification-banner.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-notification-banner/configurator-overview-notification-banner.component.spec.ts index 0bb4eee1553..7bb3505de95 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-notification-banner/configurator-overview-notification-banner.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-notification-banner/configurator-overview-notification-banner.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { TranslatePipe, UrlPipe } from '@spartacus/core'; @@ -126,7 +126,7 @@ class MockActivatedRoute { } describe('ConfigOverviewNotificationBannerComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [RouterModule, ConfiguratorOverviewNotificationBannerComponent], providers: [ @@ -150,7 +150,7 @@ describe('ConfigOverviewNotificationBannerComponent', () => { }, }) .compileComponents(); - })); + }); it('should create', () => { configurationObs = of(productConfiguration); diff --git a/feature-libs/product-configurator/rulebased/components/overview-sidebar/configurator-overview-sidebar.component.spec.ts b/feature-libs/product-configurator/rulebased/components/overview-sidebar/configurator-overview-sidebar.component.spec.ts index 21547ed206f..ea8fd0037a6 100644 --- a/feature-libs/product-configurator/rulebased/components/overview-sidebar/configurator-overview-sidebar.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/overview-sidebar/configurator-overview-sidebar.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MockTranslatePipe, @@ -24,6 +24,7 @@ import { ConfiguratorOverviewFilterComponent } from '../overview-filter/configur import { ConfiguratorOverviewMenuComponent } from '../overview-menu/configurator-overview-menu.component'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorOverviewSidebarComponent } from './configurator-overview-sidebar.component'; +import { vi } from 'vitest'; const OWNER: CommonConfigurator.Owner = ConfigurationTestData.productConfiguration.owner; @@ -45,17 +46,16 @@ function initTestComponent() { htmlElem = fixture.nativeElement; component = fixture.componentInstance; component.ghostStyle = false; - fixture.detectChanges(); configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService ); - spyOn(configuratorStorefrontUtilsService, 'getElement').and.callThrough(); - spyOn(configuratorStorefrontUtilsService, 'changeStyling').and.stub(); - spyOn( + vi.spyOn(configuratorStorefrontUtilsService, 'getElement'); + vi.spyOn( configuratorStorefrontUtilsService, - 'getSpareViewportHeight' - ).and.callThrough(); + 'changeStyling' + ).mockImplementation(() => {}); + vi.spyOn(configuratorStorefrontUtilsService, 'getSpareViewportHeight'); } class MockConfiguratorCommonsService { @@ -114,7 +114,7 @@ class MockConfiguratorOverviewMenuComponent { } describe('ConfiguratorOverviewSidebarComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorOverviewSidebarComponent], providers: [ @@ -158,13 +158,15 @@ describe('ConfiguratorOverviewSidebarComponent', () => { }) .compileComponents(); initTestComponent(); - })); + }); it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should render overview menu component by default', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -173,6 +175,7 @@ describe('ConfiguratorOverviewSidebarComponent', () => { }); it('should render overview filter component when filter tab is selected', () => { + fixture.detectChanges(); // click filter button fixture.debugElement .queryAll(By.css('.cx-menu-bar button'))[1] @@ -186,6 +189,7 @@ describe('ConfiguratorOverviewSidebarComponent', () => { }); it('should render overview filter component when filter tab is selected by enter-key', () => { + fixture.detectChanges(); // keypress on filter button fixture.debugElement .queryAll(By.css('.cx-menu-bar button'))[1] @@ -199,6 +203,7 @@ describe('ConfiguratorOverviewSidebarComponent', () => { }); it('should render overview filter component when filter tab is selected by space-key', () => { + fixture.detectChanges(); // keypress on filter button fixture.debugElement .queryAll(By.css('.cx-menu-bar button'))[1] @@ -211,10 +216,15 @@ describe('ConfiguratorOverviewSidebarComponent', () => { ); }); - it('should render overview menu component when menu tab is selected', () => { - component.onFilter(); + it('should render overview menu component when menu tab is selected', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + // Switch to filter tab first + fixture.debugElement + .queryAll(By.css('.cx-menu-bar button'))[1] + .triggerEventHandler('click'); fixture.detectChanges(); - // click menu button + // click menu button to switch back fixture.debugElement .queryAll(By.css('.cx-menu-bar button'))[0] .triggerEventHandler('click'); @@ -226,8 +236,13 @@ describe('ConfiguratorOverviewSidebarComponent', () => { ); }); - it('should render overview menu component when menu tab is selected by enter-key', () => { - component.onFilter(); + it('should render overview menu component when menu tab is selected by enter-key', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + // Switch to filter tab first + fixture.debugElement + .queryAll(By.css('.cx-menu-bar button'))[1] + .triggerEventHandler('click'); fixture.detectChanges(); // keypress on menu button fixture.debugElement @@ -241,8 +256,13 @@ describe('ConfiguratorOverviewSidebarComponent', () => { ); }); - it('should render overview menu component when menu tab is selected by space-key', () => { - component.onFilter(); + it('should render overview menu component when menu tab is selected by space-key', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + // Switch to filter tab first + fixture.debugElement + .queryAll(By.css('.cx-menu-bar button'))[1] + .triggerEventHandler('click'); fixture.detectChanges(); // keypress on menu button fixture.debugElement diff --git a/feature-libs/product-configurator/rulebased/components/previous-next-buttons/configurator-previous-next-buttons.component.spec.ts b/feature-libs/product-configurator/rulebased/components/previous-next-buttons/configurator-previous-next-buttons.component.spec.ts index 9a79d874c56..073e625e9a8 100644 --- a/feature-libs/product-configurator/rulebased/components/previous-next-buttons/configurator-previous-next-buttons.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/previous-next-buttons/configurator-previous-next-buttons.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Directive, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { I18nTestingModule, @@ -22,6 +22,7 @@ import { GROUP_ID_1, PRODUCT_CODE } from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorPreviousNextButtonsComponent } from './configurator-previous-next-buttons.component'; +import { vi } from 'vitest'; let routerStateObservable: any = null; @@ -109,7 +110,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { let configuratorUtils: CommonConfiguratorUtilsService; let configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { routerStateObservable = of(ConfigurationTestData.mockRouterState); TestBed.configureTestingModule({ imports: [ @@ -142,7 +143,11 @@ describe('ConfigPreviousNextButtonsComponent', () => { }, }) .compileComponents(); - })); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorPreviousNextButtonsComponent); @@ -154,7 +159,6 @@ describe('ConfigPreviousNextButtonsComponent', () => { configurationGroupsService = TestBed.inject( ConfiguratorGroupsService as Type ); - fixture.detectChanges(); configuratorUtils = TestBed.inject( CommonConfiguratorUtilsService as Type ); @@ -163,18 +167,16 @@ describe('ConfigPreviousNextButtonsComponent', () => { configuratorStorefrontUtilsService = TestBed.inject( ConfiguratorStorefrontUtilsService as Type ); - spyOn( - configuratorStorefrontUtilsService, - 'focusFirstAttribute' - ).and.callThrough(); + vi.spyOn(configuratorStorefrontUtilsService, 'focusFirstAttribute'); }); it('should create', () => { + fixture.detectChanges(); expect(classUnderTest).toBeTruthy(); }); it("should not display 'previous' & 'next' buttons in case configuration contains one group", () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configWithSingleGroup) ); fixture = TestBed.createComponent(ConfiguratorPreviousNextButtonsComponent); @@ -184,7 +186,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { }); it('should display previous button as disabled if it is the first group', () => { - spyOn(configurationGroupsService, 'getPreviousGroupId').and.returnValue( + vi.spyOn(configurationGroupsService, 'getPreviousGroupId').mockReturnValue( of(null) ); fixture.detectChanges(); @@ -195,9 +197,12 @@ describe('ConfigPreviousNextButtonsComponent', () => { }); it('should display previous button as enabled if it is not the first group', () => { - spyOn(configurationGroupsService, 'getPreviousGroupId').and.returnValue( + vi.spyOn(configurationGroupsService, 'getPreviousGroupId').mockReturnValue( of('anyGroupId') ); + fixture = TestBed.createComponent(ConfiguratorPreviousNextButtonsComponent); + classUnderTest = fixture.componentInstance; + htmlElem = fixture.nativeElement; fixture.detectChanges(); const prevBtn = fixture.debugElement.query( By.css('.cx-previous') @@ -206,7 +211,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { }); it('should display next button as disabled if it is the last group', () => { - spyOn(configurationGroupsService, 'getNextGroupId').and.returnValue( + vi.spyOn(configurationGroupsService, 'getNextGroupId').mockReturnValue( of(null) ); fixture.detectChanges(); @@ -217,9 +222,12 @@ describe('ConfigPreviousNextButtonsComponent', () => { }); it('should display next button as enabled if it is not the last group', () => { - spyOn(configurationGroupsService, 'getNextGroupId').and.returnValue( + vi.spyOn(configurationGroupsService, 'getNextGroupId').mockReturnValue( of('anyGroupId') ); + fixture = TestBed.createComponent(ConfiguratorPreviousNextButtonsComponent); + classUnderTest = fixture.componentInstance; + htmlElem = fixture.nativeElement; fixture.detectChanges(); const prevBtn = fixture.debugElement.query( By.css('.cx-next') @@ -234,7 +242,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { c: null, }); - spyOn(configurationGroupsService, 'getNextGroupId').and.returnValue( + vi.spyOn(configurationGroupsService, 'getNextGroupId').mockReturnValue( nextGroup ); @@ -256,7 +264,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { e: ' ', }); - spyOn(configurationGroupsService, 'getPreviousGroupId').and.returnValue( + vi.spyOn(configurationGroupsService, 'getPreviousGroupId').mockReturnValue( previousGroup ); @@ -272,44 +280,43 @@ describe('ConfigPreviousNextButtonsComponent', () => { }); it('should navigate to group exactly one time on navigateToPreviousGroup', () => { - const previousGroup = cold('-a-b|', { - a: ConfigurationTestData.GROUP_ID_1, - b: ConfigurationTestData.GROUP_ID_2, - }); + getTestScheduler().run(({ cold: coldFn, flush }) => { + const previousGroup = coldFn('-a-b|', { + a: ConfigurationTestData.GROUP_ID_1, + b: ConfigurationTestData.GROUP_ID_2, + }); - spyOn(configurationGroupsService, 'getPreviousGroupId').and.returnValue( - previousGroup - ); - spyOn(configurationGroupsService, 'navigateToGroup'); - - classUnderTest.onPrevious(config); - previousGroup.subscribe({ - complete: () => { - expect( - configurationGroupsService.navigateToGroup - ).toHaveBeenCalledTimes(1); - }, + vi.spyOn( + configurationGroupsService, + 'getPreviousGroupId' + ).mockReturnValue(previousGroup); + vi.spyOn(configurationGroupsService, 'navigateToGroup'); + + classUnderTest.onPrevious(config); + flush(); + expect(configurationGroupsService.navigateToGroup).toHaveBeenCalledTimes( + 1 + ); }); }); it('should navigate to group exactly one time on navigateToNextGroup', () => { - const nextGroup = cold('-a-b|', { - a: ConfigurationTestData.GROUP_ID_1, - b: ConfigurationTestData.GROUP_ID_2, - }); + getTestScheduler().run(({ cold: coldFn, flush }) => { + const nextGroup = coldFn('-a-b|', { + a: ConfigurationTestData.GROUP_ID_1, + b: ConfigurationTestData.GROUP_ID_2, + }); - spyOn(configurationGroupsService, 'getNextGroupId').and.returnValue( - nextGroup - ); - spyOn(configurationGroupsService, 'navigateToGroup'); - - classUnderTest.onNext(config); - nextGroup.subscribe({ - complete: () => { - expect( - configurationGroupsService.navigateToGroup - ).toHaveBeenCalledTimes(1); - }, + vi.spyOn(configurationGroupsService, 'getNextGroupId').mockReturnValue( + nextGroup + ); + vi.spyOn(configurationGroupsService, 'navigateToGroup'); + + classUnderTest.onNext(config); + flush(); + expect(configurationGroupsService.navigateToGroup).toHaveBeenCalledTimes( + 1 + ); }); }); @@ -321,10 +328,10 @@ describe('ConfigPreviousNextButtonsComponent', () => { a: true, b: false, }); - spyOn( + vi.spyOn( configuratorCommonsService, 'isConfigurationLoading' - ).and.returnValue(configurationLoading); + ).mockReturnValue(configurationLoading); classUnderTest['focusFirstAttribute'](); flush(); expect( @@ -335,6 +342,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { describe('Accessibility', () => { it("should contain action button element with 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -348,6 +356,7 @@ describe('ConfigPreviousNextButtonsComponent', () => { }); it("should contain secondary button element with 'aria-label' attribute that defines an accessible name to label the current element", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/price-summary/configurator-price-summary.component.spec.ts b/feature-libs/product-configurator/rulebased/components/price-summary/configurator-price-summary.component.spec.ts index 432c147293c..b38a0f03dec 100644 --- a/feature-libs/product-configurator/rulebased/components/price-summary/configurator-price-summary.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/price-summary/configurator-price-summary.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule, RouterState, @@ -77,7 +77,7 @@ describe('ConfigPriceSummaryComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { routerStateObservable = of(mockRouterState); TestBed.configureTestingModule({ imports: [ @@ -102,20 +102,21 @@ describe('ConfigPriceSummaryComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { config = { ...defaultConfig }; fixture = TestBed.createComponent(ConfiguratorPriceSummaryComponent); component = fixture.componentInstance; htmlElem = fixture.nativeElement; - fixture.detectChanges(); }); it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should get product code and prices as part of product configuration', () => { + fixture.detectChanges(); component.configuration$ .subscribe((data: Configurator.Configuration) => { expect(data.productCode).toEqual(PRODUCT_CODE); @@ -127,6 +128,7 @@ describe('ConfigPriceSummaryComponent', () => { }); it('should render price summary container', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -145,6 +147,7 @@ describe('ConfigPriceSummaryComponent', () => { }); it('should render selected and options price when no setting specified', () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, diff --git a/feature-libs/product-configurator/rulebased/components/price/configurator-price.component.spec.ts b/feature-libs/product-configurator/rulebased/components/price/configurator-price.component.spec.ts index d74fe6aa6d4..fe4165e1e92 100644 --- a/feature-libs/product-configurator/rulebased/components/price/configurator-price.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/price/configurator-price.component.spec.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CxNumericPipe, MockTranslatePipe, @@ -9,6 +9,7 @@ import { DirectionMode, DirectionService } from '@spartacus/storefront'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorPriceComponent } from './configurator-price.component'; +import { vi } from 'vitest'; @Pipe({ name: 'cxNumeric' }) class MockNumericPipe implements PipeTransform { @@ -41,7 +42,7 @@ describe('ConfiguratorPriceComponent', () => { let htmlElem: HTMLElement; let directionService: DirectionService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ConfiguratorPriceComponent], providers: [ @@ -60,7 +61,7 @@ describe('ConfiguratorPriceComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorPriceComponent); @@ -78,7 +79,7 @@ describe('ConfiguratorPriceComponent', () => { describe('Display quantity', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.LTR ); }); @@ -110,7 +111,7 @@ describe('ConfiguratorPriceComponent', () => { describe('Display value price', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.LTR ); }); @@ -199,7 +200,7 @@ describe('ConfiguratorPriceComponent', () => { describe('Display total price', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.LTR ); }); @@ -264,7 +265,7 @@ describe('ConfiguratorPriceComponent', () => { describe('isPriceLightedUp', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.LTR ); }); @@ -288,7 +289,7 @@ describe('ConfiguratorPriceComponent', () => { describe('LTR direction', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.LTR ); }); @@ -324,7 +325,7 @@ describe('ConfiguratorPriceComponent', () => { describe('RTL direction', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.RTL ); }); @@ -376,7 +377,7 @@ describe('ConfiguratorPriceComponent', () => { describe('Accessibility', () => { beforeEach(() => { - spyOn(directionService, 'getDirection').and.returnValue( + vi.spyOn(directionService, 'getDirection').mockReturnValue( DirectionMode.LTR ); }); diff --git a/feature-libs/product-configurator/rulebased/components/product-title/configurator-product-title.component.spec.ts b/feature-libs/product-configurator/rulebased/components/product-title/configurator-product-title.component.spec.ts index 64e23052570..36ddf88f8c3 100644 --- a/feature-libs/product-configurator/rulebased/components/product-title/configurator-product-title.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/product-title/configurator-product-title.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -30,6 +30,7 @@ import { ConfiguratorExpertModeService } from '../../core/services/configurator- import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorProductTitleComponent } from './configurator-product-title.component'; +import { vi } from 'vitest'; const mockProductConfiguration = ConfigurationTestData.productConfiguration; const PRODUCT_CODE = ConfigurationTestData.PRODUCT_CODE; @@ -199,7 +200,6 @@ function initialize() { htmlElem = fixture.nativeElement; component = fixture.componentInstance; component.ghostStyle = false; - fixture.detectChanges(); } function setDataForProductConfiguration() { @@ -328,7 +328,7 @@ function setDataForQuoteEntry() { } describe('ConfigProductTitleComponent', () => { - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -372,23 +372,25 @@ describe('ConfigProductTitleComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { mockRouterData = structuredClone(baseMockRouterData); initialize(); + fixture.detectChanges(); configExpertModeService = TestBed.inject(ConfiguratorExpertModeService); - spyOn(configExpertModeService, 'setExpModeRequested').and.callThrough(); - spyOn(configExpertModeService, 'setExpModeActive').and.callThrough(); + vi.spyOn(configExpertModeService, 'setExpModeRequested'); + vi.spyOn(configExpertModeService, 'setExpModeActive'); productService = TestBed.inject(ProductService); - spyOn(productService, 'get').and.returnValue(productObservable); + vi.spyOn(productService, 'get').mockReturnValue(productObservable); }); it('should create component', () => { setDataForProductConfiguration(); initialize(); + fixture.detectChanges(); expect(component).toBeDefined(); }); @@ -396,6 +398,7 @@ describe('ConfigProductTitleComponent', () => { it('should get product name as part of product configuration via config product code', () => { setDataForProductConfiguration(); initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( PRODUCT_CODE, @@ -407,6 +410,7 @@ describe('ConfigProductTitleComponent', () => { setDataForProductConfiguration(); mockRouterData.productCode = PRODUCT_SUFFIX + PRODUCT_CODE; initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( mockRouterData.productCode, @@ -419,6 +423,7 @@ describe('ConfigProductTitleComponent', () => { mockConfiguration.productCode = PRODUCT_CODE; mockRouterData.productCode = undefined; initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( PRODUCT_CODE, @@ -432,6 +437,7 @@ describe('ConfigProductTitleComponent', () => { // provided via routing data. setDataForCartEntry(); initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( PRODUCT_CODE, @@ -446,6 +452,7 @@ describe('ConfigProductTitleComponent', () => { // entry which has been re-read after a preceding entry was deleted. mockRouterData.productCode = CART_ENTRY_SUFFIX + 'STALE_PRODUCT'; initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( PRODUCT_CODE, @@ -458,6 +465,7 @@ describe('ConfigProductTitleComponent', () => { mockConfiguration.productCode = undefined as unknown as string; mockConfiguration.overview = undefined; initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( CART_ENTRY_SUFFIX + PRODUCT_CODE, @@ -470,6 +478,7 @@ describe('ConfigProductTitleComponent', () => { mockConfiguration.productCode = PRODUCT_CODE; mockRouterData.productCode = undefined; initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( PRODUCT_CODE, @@ -480,6 +489,7 @@ describe('ConfigProductTitleComponent', () => { it('should get product name as part of product configuration in case configuration is saved cart bound and product code is provided with routing data', () => { setDataForSavedCartEntry(); initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( SAVED_CART_ENTRY_SUFFIX + PRODUCT_CODE, @@ -492,6 +502,7 @@ describe('ConfigProductTitleComponent', () => { mockConfiguration.productCode = PRODUCT_CODE; mockRouterData.productCode = undefined; initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( PRODUCT_CODE, @@ -502,6 +513,7 @@ describe('ConfigProductTitleComponent', () => { it('should get product name as part of product configuration in case configuration is quote bound and product code is provided with routing data', () => { setDataForQuoteEntry(); initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( QUOTE_ENTRY_SUFFIX + PRODUCT_CODE, @@ -512,6 +524,7 @@ describe('ConfigProductTitleComponent', () => { it('should get product name as part of product from overview in case configuration is order bound and product code is not provided with routing data', () => { setDataForOrderEntry(); initialize(); + fixture.detectChanges(); expect(productService.get).toHaveBeenCalledWith( ORDER_ENTRY_SUFFIX + PRODUCT_CODE, @@ -523,6 +536,7 @@ describe('ConfigProductTitleComponent', () => { it('should render initial content properly', () => { setDataForProductConfiguration(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -551,6 +565,7 @@ describe('ConfigProductTitleComponent', () => { it('should render show more case - default', () => { setDataForProductConfiguration(); initialize(); + fixture.detectChanges(); component.triggerDetails(); changeDetectorRef.detectChanges(); @@ -572,6 +587,7 @@ describe('ConfigProductTitleComponent', () => { it('should render properly for navigation from order entry', () => { setDataForOrderEntry(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, htmlElem, @@ -588,6 +604,7 @@ describe('ConfigProductTitleComponent', () => { it('should render kb key details properly', () => { setDataForProductConfiguration(); initialize(); + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementPresent( expect, @@ -653,6 +670,7 @@ describe('ConfigProductTitleComponent', () => { beforeEach(() => { setDataForProductConfiguration(); initialize(); + fixture.detectChanges(); }); it("should contain cx-icon element with an 'aria-label' attribute that defines an accessible name to label the current element", () => { diff --git a/feature-libs/product-configurator/rulebased/components/restart-dialog/configurator-restart-dialog.component.spec.ts b/feature-libs/product-configurator/rulebased/components/restart-dialog/configurator-restart-dialog.component.spec.ts index 2f1096758f0..17d41f6f08d 100644 --- a/feature-libs/product-configurator/rulebased/components/restart-dialog/configurator-restart-dialog.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/restart-dialog/configurator-restart-dialog.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Directive, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MockTranslatePipe, @@ -16,11 +16,12 @@ import { ICON_TYPE, LaunchDialogService, } from '@spartacus/storefront'; -import { BehaviorSubject, of } from 'rxjs'; +import { BehaviorSubject, lastValueFrom, of } from 'rxjs'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service'; import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorRestartDialogComponent } from './configurator-restart-dialog.component'; +import { vi } from 'vitest'; const owner: CommonConfigurator.Owner = ConfigurationTestData.productConfiguration.owner; @@ -61,9 +62,8 @@ describe('ConfiguratorRestartDialogComponent', () => { fixture = TestBed.createComponent(ConfiguratorRestartDialogComponent); htmlElem = fixture.nativeElement; component = fixture.componentInstance; - fixture.detectChanges(); mockLaunchDialogService = TestBed.inject(LaunchDialogService); - spyOn(mockLaunchDialogService, 'closeDialog'); + vi.spyOn(mockLaunchDialogService, 'closeDialog'); } class MockLaunchDialogService { @@ -72,17 +72,13 @@ describe('ConfiguratorRestartDialogComponent', () => { } function initializeMocks() { - mockConfigCommonsService = jasmine.createSpyObj(['forceNewConfiguration']); - mockRoutingService = jasmine.createSpyObj(['go']); - mockProductService = jasmine.createSpyObj(['get']); - asSpy(mockProductService.get).and.returnValue(of(product)); - } - - function asSpy(f: any) { - return f; + mockConfigCommonsService = { forceNewConfiguration: vi.fn() }; + mockRoutingService = { go: vi.fn() }; + mockProductService = { get: vi.fn() }; + mockProductService.get.mockReturnValue(of(product)); } - beforeEach(waitForAsync(() => { + beforeEach(async () => { initializeMocks(); TestBed.configureTestingModule({ imports: [ConfiguratorRestartDialogComponent], @@ -115,23 +111,25 @@ describe('ConfiguratorRestartDialogComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { initialize(); + fixture.detectChanges(); }); it('should create component', () => { expect(component).toBeDefined(); }); - it('should ignore invalid dialog data', (done) => { - component.dialogData$.subscribe({ - next: (dialogData: any) => expect(dialogData).toBeDefined(), - complete: done, - }); + it('should ignore invalid dialog data', async () => { dialogDataSender.next(undefined); dialogDataSender.complete(); + const dialogData = await lastValueFrom(component.dialogData$, { + defaultValue: undefined, + }); + // dialogData$ filters out undefined, so completion without emit is valid + expect(true).toBe(true); // stream completed without error }); it('should query product by owner id', () => { diff --git a/feature-libs/product-configurator/rulebased/components/service/configurator-storefront-utils.service.spec.ts b/feature-libs/product-configurator/rulebased/components/service/configurator-storefront-utils.service.spec.ts index 8740bed0006..d192705a2e4 100644 --- a/feature-libs/product-configurator/rulebased/components/service/configurator-storefront-utils.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/service/configurator-storefront-utils.service.spec.ts @@ -19,6 +19,7 @@ import { ConfiguratorGroupsService } from '../../core/facade/configurator-groups import { Configurator } from '../../core/model/configurator.model'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from './configurator-storefront-utils.service'; +import { vi } from 'vitest'; let mockedWindow: { innerWidth?: number; @@ -176,7 +177,8 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); afterEach(() => { - if (htmlElem) { + vi.restoreAllMocks(); + if (htmlElem && document.body.contains(htmlElem)) { document.body.removeChild(htmlElem); } }); @@ -196,14 +198,14 @@ describe('ConfiguratorStorefrontUtilsService', () => { it('should scroll to element', () => { const theElement = document.createElement('div'); - spyOn(windowRef.document, 'querySelector').and.returnValue(theElement); - spyOn(theElement, 'getBoundingClientRect').and.returnValue( + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(theElement); + vi.spyOn(theElement, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 2000, 100, 100) ); - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const nativeWindow = windowRef.nativeWindow; if (nativeWindow) { - spyOn(nativeWindow, 'scroll').and.callThrough(); + vi.spyOn(nativeWindow, 'scroll'); classUnderTest.scrollToConfigurationElement( '.VariantConfigurationTemplate' ); @@ -291,7 +293,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('scroll', () => { it('should handle situation that we are not in browser environment', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); classUnderTest['scroll'](fixture.debugElement.nativeElement); expect(windowRef.nativeWindow).toBeUndefined(); }); @@ -328,12 +330,12 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('Focused elements', () => { describe('focusFirstActiveElement', () => { it('should delegate to keyboard focus service', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const focusedElements = createFocusedElements('ATTR', 2, 3); - spyOn(windowRef.document, 'querySelector').and.returnValue( + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue( focusedElements[0] ); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); classUnderTest.focusFirstActiveElement('elementSelector'); @@ -341,27 +343,29 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); it('should not delegate to keyboard focus service because form is undefined', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); - spyOn(windowRef.document, 'querySelector').and.returnValue(undefined); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue([]); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue( + undefined + ); + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue([]); classUnderTest.focusFirstActiveElement('elementSelector'); expect(keyboardFocusService.findFocusable).toHaveBeenCalledTimes(0); }); it('should not delegate to keyboard focus service because there are no focused elements in form', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); - spyOn(keyboardFocusService, 'findFocusable').and.callThrough(); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); + vi.spyOn(keyboardFocusService, 'findFocusable'); classUnderTest.focusFirstActiveElement('elementSelector'); expect(keyboardFocusService.findFocusable).toHaveBeenCalledTimes(0); }); it('should not delegate to keyboard focus service because keyboard focus service returns no focusable elements', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const focusedElements = createFocusedElements('ATTR', 2, 3); - spyOn(windowRef.document, 'querySelector').and.returnValue( + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue( focusedElements[0] ); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue([]); + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue([]); classUnderTest.focusFirstActiveElement('elementSelector'); expect(keyboardFocusService.findFocusable).toHaveBeenCalledTimes(1); }); @@ -382,7 +386,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { function spyFocusForFocusedElements(focusedElements: any) { focusedElements.forEach((focusedElement: any) => { - spyOn(focusedElement, 'focus').and.callThrough(); + vi.spyOn(focusedElement, 'focus'); }); } @@ -413,15 +417,15 @@ describe('ConfiguratorStorefrontUtilsService', () => { focusedElements = fixture.debugElement .queryAll(By.css('label')) .map((el) => el.nativeNode); - spyOn(windowRef.document, 'querySelector').and.returnValue( + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue( focusedElements ); }); it('should set focus because attribute exists', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); spyFocusForFocusedElements(focusedElements); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); @@ -430,9 +434,9 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); it('should set focus because attribute contains selected value', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); spyFocusForFocusedElements(focusedElements); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); @@ -446,12 +450,12 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); it('should set focus because on conflict description', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); focusedElements = [ createNode('cx-configurator-conflict-description'), ].concat(focusedElements); spyFocusForFocusedElements(focusedElements); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); @@ -465,9 +469,9 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); it('should not set focus because no focused element is found', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); spyFocusForFocusedElements(focusedElements); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); attribute.name = 'NO_ATTR_2'; @@ -477,21 +481,21 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); it('should not set focus because form is not defined', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); spyFocusForFocusedElements(focusedElements); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); - asSpy(windowRef.document.querySelector).and.returnValue(undefined); + asSpy(windowRef.document.querySelector).mockReturnValue(undefined); classUnderTest.focusValue(attribute); verify(focusedElements); }); it('should not set focus because browser context is not defined', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); spyFocusForFocusedElements(focusedElements); - spyOn(keyboardFocusService, 'findFocusable').and.returnValue( + vi.spyOn(keyboardFocusService, 'findFocusable').mockReturnValue( focusedElements ); @@ -531,19 +535,19 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('setFocus', () => { it('should not call keyboard focus service to set focus because no parameters are defined', () => { - spyOn(keyboardFocusService, 'set').and.callThrough(); + vi.spyOn(keyboardFocusService, 'set'); classUnderTest.setFocus(); expect(keyboardFocusService.set).toHaveBeenCalledTimes(0); }); it('should not call keyboard focus service to set focus because key is not defined', () => { - spyOn(keyboardFocusService, 'set').and.callThrough(); + vi.spyOn(keyboardFocusService, 'set'); classUnderTest.setFocus(undefined, 'GR_01'); expect(keyboardFocusService.set).toHaveBeenCalledTimes(0); }); it('should call keyboard focus service to set focus because key is defined', () => { - spyOn(keyboardFocusService, 'set').and.callThrough(); + vi.spyOn(keyboardFocusService, 'set'); classUnderTest.setFocus('key'); expect(keyboardFocusService.set).toHaveBeenCalledTimes(1); }); @@ -551,14 +555,14 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('getElement', () => { it('should not get HTML element based on query selector', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getElement('elementMock')).toBeUndefined(); }); it('should get HTML element based on query selector', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const theElement = document.createElement('elementMock'); - spyOn(windowRef.document, 'querySelector').and.returnValue(theElement); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(theElement); expect(classUnderTest.getElement('elementMock')).toEqual(theElement); }); @@ -567,7 +571,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('changeStyling', () => { it('should change styling of HTML element', () => { const theElement = document.createElement('elementMock'); - spyOn(windowRef.document, 'querySelector').and.returnValue(undefined); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(undefined); classUnderTest.changeStyling('elementMock', 'position', 'sticky'); expect(theElement.style.position).not.toEqual('sticky'); @@ -575,7 +579,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { it('should change styling of HTML element', () => { const theElement = document.createElement('elementMock'); - spyOn(windowRef.document, 'querySelector').and.returnValue(theElement); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(theElement); classUnderTest.changeStyling('elementMock', 'position', 'sticky'); expect(theElement.style.position).toEqual('sticky'); @@ -584,20 +588,20 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('removeStyling', () => { it('should not remove styling of HTML element', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const theElement = document.createElement('elementMock'); theElement.style.position = 'sticky'; - spyOn(windowRef.document, 'querySelector').and.returnValue(undefined); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(undefined); classUnderTest.removeStyling('elementMock', 'position'); expect(theElement.style.position).toEqual('sticky'); }); it('should remove styling of HTML element', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const theElement = document.createElement('elementMock'); theElement.style.position = 'sticky'; - spyOn(windowRef.document, 'querySelector').and.returnValue(theElement); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(theElement); classUnderTest.removeStyling('elementMock', 'position'); expect(theElement.style.position).toBe(''); @@ -606,7 +610,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('getElements', () => { it('should not get HTML elements based on query selector', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getElements('elementMock')).toBeUndefined(); }); @@ -624,10 +628,10 @@ describe('ConfiguratorStorefrontUtilsService', () => { } it('should return HTML element based on query selector', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const elements: Array = createElements('section', 10); - spyOn(document, 'querySelectorAll').and.returnValue(elements); + vi.spyOn(document, 'querySelectorAll').mockReturnValue(elements); const htmlElements = classUnderTest.getElements('section'); @@ -641,12 +645,12 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('getVerticallyScrolledPixels', () => { it('should return undefined', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getVerticallyScrolledPixels()).toBeUndefined(); }); it('should return number of pixels that the document is currently scrolled vertically', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); mockedWindow.scrollY = 250; const nativeWindow = windowRef.nativeWindow; if (nativeWindow) { @@ -657,7 +661,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { describe('hasScrollbar', () => { it('should return false because element is undefined', () => { - spyOn(windowRef.document, 'querySelector').and.returnValue(undefined); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(undefined); expect(classUnderTest.hasScrollbar('elementMock')).toBe(false); }); @@ -694,6 +698,9 @@ describe('ConfiguratorStorefrontUtilsService', () => { form.style.display = 'flex'; form.style.flexDirection = 'column'; + vi.spyOn(form, 'scrollHeight', 'get').mockReturnValue(200); + vi.spyOn(form, 'clientHeight', 'get').mockReturnValue(50); + expect(classUnderTest.hasScrollbar('cx-configurator-form')).toBe(true); }); }); @@ -714,7 +721,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { label.style.height = '50px'; }); - spyOn(form, 'getBoundingClientRect').and.returnValue( + vi.spyOn(form, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 100, 250, 500) ); }); @@ -749,6 +756,9 @@ describe('ConfiguratorStorefrontUtilsService', () => { mockedWindow.innerWidth = undefined; + vi.spyOn(form, 'clientWidth', 'get').mockReturnValue(400); + vi.spyOn(form, 'offsetWidth', 'get').mockReturnValue(400); + expect(classUnderTest['isInViewport'](form)).toBe(true); }); @@ -759,6 +769,11 @@ describe('ConfiguratorStorefrontUtilsService', () => { mockedWindow.innerHeight = undefined; + vi.spyOn(form, 'clientHeight', 'get').mockReturnValue(700); + vi.spyOn(form, 'offsetHeight', 'get').mockReturnValue(700); + vi.spyOn(form, 'clientWidth', 'get').mockReturnValue(400); + vi.spyOn(form, 'offsetWidth', 'get').mockReturnValue(400); + expect(classUnderTest['isInViewport'](form)).toBe(true); }); }); @@ -772,7 +787,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { form.style.height = '50px'; form.style.border = 'thick double #32a1ce;'; - spyOn(form, 'getBoundingClientRect').and.returnValue( + vi.spyOn(form, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 100, 250, 500) ); }); @@ -790,6 +805,8 @@ describe('ConfiguratorStorefrontUtilsService', () => { it('should return offsetHeight of the element because form is not in viewport', () => { mockedWindow.innerWidth = 1000; + vi.spyOn(form, 'offsetHeight', 'get').mockReturnValue(50); + expect( classUnderTest['getHeight']('cx-configurator-form') ).toBeGreaterThan(0); @@ -808,7 +825,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); function createTestData() { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); spaHeader = document.createElement('header'); document.body.append(spaHeader); @@ -823,17 +840,19 @@ describe('ConfiguratorStorefrontUtilsService', () => { } it('should return zero because isBrowser is undefined', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getSpareViewportHeight()).toBe(0); }); it('should return zero because isBrowser is undefined', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getSpareViewportHeight()).toBe(0); }); it('should return zero because nativeWindow is undefined', () => { - spyOn(windowRef, 'isBrowser').and.returnValues(true, false); + vi.spyOn(windowRef, 'isBrowser') + .mockReturnValueOnce(true) + .mockReturnValue(false); expect(classUnderTest.getSpareViewportHeight()).toBe(0); }); @@ -846,10 +865,10 @@ describe('ConfiguratorStorefrontUtilsService', () => { createTestData(); addToCart.style.padding = '20px'; addToCart.style.height = '80px'; - spyOn(addToCart, 'getBoundingClientRect').and.returnValue( + vi.spyOn(addToCart, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 100, 1000, 80) ); - spyOn(classUnderTest, 'getHeight').and.returnValue(100); + vi.spyOn(classUnderTest, 'getHeight').mockReturnValue(100); expect(classUnderTest.getSpareViewportHeight()).toBeGreaterThan(0); }); @@ -870,29 +889,29 @@ describe('ConfiguratorStorefrontUtilsService', () => { ?.getBoundingClientRect(); const height: number = documentHeight ? documentHeight.height : 0; const elementOffsetHeight = height - offsetHeight; - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); ovMenu = document.createElement(testSelector); document.body.append(ovMenu); - spyOnProperty(ovMenu, 'offsetHeight').and.returnValue( + vi.spyOn(ovMenu, 'offsetHeight', 'get').mockReturnValue( elementOffsetHeight ); - spyOnProperty(ovMenu, 'scrollTop').and.returnValue(150); + vi.spyOn(ovMenu, 'scrollTop', 'get').mockReturnValue(150); menuItem = document.createElement('button'); document.body.append(menuItem); menuItem.className = 'cx-menu-item'; - spyOn(menuItem, 'getBoundingClientRect').and.returnValue( + vi.spyOn(menuItem, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 100, 100, 25) ); - spyOnProperty(menuItem, 'offsetTop').and.returnValue(offsetTop); - spyOnProperty(menuItem, 'offsetHeight').and.returnValue(50); + vi.spyOn(menuItem, 'offsetTop', 'get').mockReturnValue(offsetTop); + vi.spyOn(menuItem, 'offsetHeight', 'get').mockReturnValue(50); } it('should not ensure visibility of the element', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); ovMenu = document.createElement('cx-configurator-overview-menu'); - spyOn(windowRef.document, 'querySelector').and.returnValue(ovMenu); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(ovMenu); classUnderTest.ensureElementVisible( 'cx-configurator-overview-menu', undefined @@ -910,7 +929,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { it('should ensure visibility of the element when element.offsetTop is greater than container.scrollTop', () => { createTestData(5000, 450); - spyOnProperty(ovMenu, 'offsetTop').and.returnValue(250); + vi.spyOn(ovMenu, 'offsetTop', 'get').mockReturnValue(250); classUnderTest.ensureElementVisible(testSelector, menuItem); ovMenu = document.querySelector(testSelector); expect(ovMenu.scrollTop).toBeGreaterThan(0); @@ -918,7 +937,7 @@ describe('ConfiguratorStorefrontUtilsService', () => { it('should ensure visibility of the element when element.offsetTop is less than container.scrollTop', () => { createTestData(5000, 50); - spyOnProperty(ovMenu, 'offsetTop').and.returnValue(50); + vi.spyOn(ovMenu, 'offsetTop', 'get').mockReturnValue(50); classUnderTest.ensureElementVisible(testSelector, menuItem); ovMenu = document.querySelector(testSelector); expect(ovMenu.scrollTop).toBeGreaterThan(0); @@ -1003,6 +1022,6 @@ describe('ConfiguratorStorefrontUtilsService', () => { }); function asSpy(f: any) { - return f; + return f; } }); diff --git a/feature-libs/product-configurator/rulebased/components/show-more/configurator-show-more.component.spec.ts b/feature-libs/product-configurator/rulebased/components/show-more/configurator-show-more.component.spec.ts index 138bbd933d6..4ce2959e2ee 100644 --- a/feature-libs/product-configurator/rulebased/components/show-more/configurator-show-more.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/show-more/configurator-show-more.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorShowMoreComponent } from './configurator-show-more.component'; @@ -9,7 +9,7 @@ describe('ConfiguratorShowMoreComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule, ConfiguratorShowMoreComponent], }) @@ -19,7 +19,7 @@ describe('ConfiguratorShowMoreComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorShowMoreComponent); diff --git a/feature-libs/product-configurator/rulebased/components/tab-bar/configurator-tab-bar.component.spec.ts b/feature-libs/product-configurator/rulebased/components/tab-bar/configurator-tab-bar.component.spec.ts index bbd5935a7ce..b5286ac45d6 100644 --- a/feature-libs/product-configurator/rulebased/components/tab-bar/configurator-tab-bar.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/tab-bar/configurator-tab-bar.component.spec.ts @@ -4,13 +4,7 @@ import { PipeTransform, Type, } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; import { @@ -32,6 +26,7 @@ import { Configurator } from '../../core/model/configurator.model'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service'; import { ConfiguratorTabBarComponent } from './configurator-tab-bar.component'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; const CONFIG_OVERVIEW_ROUTE = 'configureOverviewCPQCONFIGURATOR'; @@ -103,7 +98,7 @@ describe('ConfigTabBarComponent', () => { let routingService: RoutingService; let keyboardFocusService: KeyboardFocusService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { mockRouterState = structuredClone(mockRouterStateBase); mockRouterState.state.params.displayOnly = false; @@ -140,7 +135,7 @@ describe('ConfigTabBarComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorTabBarComponent); component = fixture.componentInstance; @@ -162,11 +157,8 @@ describe('ConfigTabBarComponent', () => { keyboardFocusService = TestBed.inject( KeyboardFocusService as Type ); - spyOn(keyboardFocusService, 'clear').and.callThrough(); - spyOn( - configuratorStorefrontUtilsService, - 'focusFirstActiveElement' - ).and.callThrough(); + vi.spyOn(keyboardFocusService, 'clear'); + vi.spyOn(configuratorStorefrontUtilsService, 'focusFirstActiveElement'); routingService = TestBed.inject(RoutingService as Type); }); @@ -490,47 +482,53 @@ describe('ConfigTabBarComponent', () => { }); describe('Focus handling on navigation', () => { - it('focusOverviewInTabBar should call clear and focusFirstActiveElement', fakeAsync(() => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it('focusOverviewInTabBar should call clear and focusFirstActiveElement', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configWithOverview) ); component['focusOverviewInTabBar'](); - tick(1); // needed because of delay(0) in focusOverviewInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusOverviewInTabBar expect(keyboardFocusService.clear).toHaveBeenCalledTimes(1); expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(1); - })); + }); - it('focusOverviewInTabBar should not call clear and focusFirstActiveElement if overview data is not present in configuration', fakeAsync(() => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('focusOverviewInTabBar should not call clear and focusFirstActiveElement if overview data is not present in configuration', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( configurationObs ); component['focusOverviewInTabBar'](); - tick(1); // needed because of delay(0) in focusOverviewInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusOverviewInTabBar expect(keyboardFocusService.clear).toHaveBeenCalledTimes(0); expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(0); - })); + }); - it('focusConfigurationInTabBar should call clear and focusFirstActiveElement', fakeAsync(() => { + it('focusConfigurationInTabBar should call clear and focusFirstActiveElement', async () => { mockRouterState.state.semanticRoute = CONFIGURATOR_ROUTE; component['focusConfigurationInTabBar'](); - tick(1); // needed because of delay(0) in focusConfigurationInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusConfigurationInTabBar expect(keyboardFocusService.clear).toHaveBeenCalledTimes(1); expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(1); - })); + }); - it('navigateToOverview should navigate to overview page and should call focusFirstActiveElement inside focusOverviewInTabBar', fakeAsync(() => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('navigateToOverview should navigate to overview page and should call focusFirstActiveElement inside focusOverviewInTabBar', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configWithOverview) ); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); component['navigateToOverview'](mockRouterData); - tick(1); // needed because of delay(0) in focusOverviewInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusOverviewInTabBar expect(routingService.go).toHaveBeenCalledWith( { cxRoute: 'configureOverview' + mockRouterData.owner.configuratorType, @@ -544,13 +542,13 @@ describe('ConfigTabBarComponent', () => { expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(1); - })); + }); - it('navigateToConfiguration should navigate to configuration page and should call focusFirstActiveElement inside focusConfigurationInTabBar', fakeAsync(() => { + it('navigateToConfiguration should navigate to configuration page and should call focusFirstActiveElement inside focusConfigurationInTabBar', async () => { mockRouterState.state.semanticRoute = CONFIGURATOR_ROUTE; - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); component['navigateToConfiguration'](mockRouterData); - tick(1); // needed because of delay(0) in focusConfigurationInTabBar + await vi.advanceTimersByTimeAsync(1); // needed because of delay(0) in focusConfigurationInTabBar expect(routingService.go).toHaveBeenCalledWith( { cxRoute: 'configure' + mockRouterData.owner.configuratorType, @@ -564,6 +562,6 @@ describe('ConfigTabBarComponent', () => { expect( configuratorStorefrontUtilsService.focusFirstActiveElement ).toHaveBeenCalledTimes(1); - })); + }); }); }); diff --git a/feature-libs/product-configurator/rulebased/components/update-message/configurator-update-message.component.spec.ts b/feature-libs/product-configurator/rulebased/components/update-message/configurator-update-message.component.spec.ts index 0252f173718..19b94a78fa3 100644 --- a/feature-libs/product-configurator/rulebased/components/update-message/configurator-update-message.component.spec.ts +++ b/feature-libs/product-configurator/rulebased/components/update-message/configurator-update-message.component.spec.ts @@ -1,11 +1,5 @@ import { Component, Type } from '@angular/core'; -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterState } from '@angular/router'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -27,6 +21,7 @@ import { ConfiguratorCommonsService } from '../../core/facade/configurator-commo import * as ConfigurationTestData from '../../testing/configurator-test-data'; import { ConfiguratorMessageConfig } from '../config/configurator-message.config'; import { ConfiguratorUpdateMessageComponent } from './configurator-update-message.component'; +import { vi } from 'vitest'; let routerStateObservable: any = null; class MockRoutingService { @@ -66,7 +61,7 @@ describe('ConfiguratorUpdateMessageComponent', () => { let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { routerStateObservable = of(ConfigurationTestData.mockRouterState); TestBed.configureTestingModule({ imports: [ @@ -96,7 +91,7 @@ describe('ConfiguratorUpdateMessageComponent', () => { imports: [MockTranslatePipe, MockDatePipe, MockCxSpinnerComponent], }, }); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorUpdateMessageComponent); htmlElem = fixture.nativeElement; @@ -107,6 +102,13 @@ describe('ConfiguratorUpdateMessageComponent', () => { configuratorUtils.setOwnerKey(owner); }); + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should create component', () => { expect(component).toBeDefined(); }); @@ -121,7 +123,7 @@ describe('ConfiguratorUpdateMessageComponent', () => { ); }); - it('should show update banner after delay time if pending changes is true', fakeAsync(() => { + it('should show update banner after delay time if pending changes is true', async () => { //Should be hidden first expect(htmlElem.querySelectorAll('div.cx-update-msg.visible').length).toBe( 0 @@ -130,16 +132,16 @@ describe('ConfiguratorUpdateMessageComponent', () => { fixture.detectChanges(); //Should appear after a bit - tick(2000); + await vi.advanceTimersByTimeAsync(2000); fixture.detectChanges(); expect(htmlElem.querySelectorAll('div.cx-update-msg.visible').length).toBe( 1 ); expect(htmlElem.querySelectorAll('div').length).toBe(2); - })); + }); - it('should show update banner after default delay time if pending changes is true and no delay time configured', fakeAsync(() => { + it('should show update banner after default delay time if pending changes is true and no delay time configured', async () => { //Should be hidden first expect(htmlElem.querySelectorAll('div.cx-update-msg.visible').length).toBe( 0 @@ -149,15 +151,15 @@ describe('ConfiguratorUpdateMessageComponent', () => { waitingTime = undefined; //Should appear after a bit - tick(2000); + await vi.advanceTimersByTimeAsync(2000); fixture.detectChanges(); expect(htmlElem.querySelectorAll('div.cx-update-msg.visible').length).toBe( 1 ); - })); + }); - it('should not show update banner if pending changes is true but delay time not reached', fakeAsync(() => { + it('should not show update banner if pending changes is true but delay time not reached', async () => { //Should be hidden first expect(htmlElem.querySelectorAll('div.cx-update-msg.visible').length).toBe( 0 @@ -166,12 +168,12 @@ describe('ConfiguratorUpdateMessageComponent', () => { fixture.detectChanges(); //Wait a bit, but don't reach delay time - tick(500); + await vi.advanceTimersByTimeAsync(500); fixture.detectChanges(); expect(htmlElem.querySelectorAll('div.cx-update-msg.visible').length).toBe( 0 ); - tick(1000); - })); + await vi.advanceTimersByTimeAsync(1000); + }); }); diff --git a/feature-libs/product-configurator/rulebased/core/connectors/rulebased-configurator.connector.spec.ts b/feature-libs/product-configurator/rulebased/core/connectors/rulebased-configurator.connector.spec.ts index 51321fb4804..4211c538faa 100644 --- a/feature-libs/product-configurator/rulebased/core/connectors/rulebased-configurator.connector.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/connectors/rulebased-configurator.connector.spec.ts @@ -13,8 +13,7 @@ import { ConfiguratorCoreConfig } from '../config/configurator-core.config'; import { Configurator } from '../model/configurator.model'; import { RulebasedConfiguratorAdapter } from './rulebased-configurator.adapter'; import { RulebasedConfiguratorConnector } from './rulebased-configurator.connector'; - -import createSpy = jasmine.createSpy; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; const CONFIG_ID = '1234-56-7890'; @@ -62,48 +61,60 @@ const cartModification: CartModification = {}; class MockRulebasedConfiguratorAdapter implements RulebasedConfiguratorAdapter { public configuratorType: string; - readConfigurationForCartEntry = createSpy().and.callFake(() => - of(productConfiguration) - ); - readConfigurationForOrderEntry = createSpy().and.callFake(() => - of(productConfiguration) - ); - updateConfigurationForCartEntry = createSpy().and.callFake(() => - of(cartModification) - ); - getConfigurationOverview = createSpy().and.callFake((configId: string) => - of('getConfigurationOverview' + configId) - ); - - searchVariants = createSpy().and.callFake((configId: string) => - of([{ productCode: PRODUCT_CODE + configId }]) - ); - - readPriceSummary = createSpy().and.callFake((configId: string) => - of('readPriceSummary' + configId) - ); - - readConfiguration = createSpy().and.callFake((configId: string) => - of('readConfiguration' + configId) - ); - - updateConfiguration = createSpy().and.callFake( - (configuration: Configurator.Configuration) => + readConfigurationForCartEntry = vi + .fn() + .mockImplementation(() => of(productConfiguration)); + readConfigurationForOrderEntry = vi + .fn() + .mockImplementation(() => of(productConfiguration)); + updateConfigurationForCartEntry = vi + .fn() + .mockImplementation(() => of(cartModification)); + getConfigurationOverview = vi + .fn() + .mockImplementation((configId: string) => + of('getConfigurationOverview' + configId) + ); + + searchVariants = vi + .fn() + .mockImplementation((configId: string) => + of([{ productCode: PRODUCT_CODE + configId }]) + ); + + readPriceSummary = vi + .fn() + .mockImplementation((configId: string) => + of('readPriceSummary' + configId) + ); + + readConfiguration = vi + .fn() + .mockImplementation((configId: string) => + of('readConfiguration' + configId) + ); + + updateConfiguration = vi + .fn() + .mockImplementation((configuration: Configurator.Configuration) => of('updateConfiguration' + configuration.configId) - ); + ); - updateConfigurationOverview = createSpy().and.callFake( - (ovInput: Configurator.Overview) => + updateConfigurationOverview = vi + .fn() + .mockImplementation((ovInput: Configurator.Overview) => of('updateConfigurationOverview' + ovInput.configId) - ); + ); - createConfiguration = createSpy().and.callFake( - (owner: CommonConfigurator.Owner) => of('createConfiguration' + owner) - ); + createConfiguration = vi + .fn() + .mockImplementation((owner: CommonConfigurator.Owner) => + of('createConfiguration' + owner) + ); - addToCart = createSpy().and.callFake((configId: string) => - of('addToCart' + configId) - ); + addToCart = vi + .fn() + .mockImplementation((configId: string) => of('addToCart' + configId)); getConfiguratorType(): string { return this.configuratorType ?? CONFIGURATOR_TYPE; } diff --git a/feature-libs/product-configurator/rulebased/core/events/configurator-language-set-event.listener.spec.ts b/feature-libs/product-configurator/rulebased/core/events/configurator-language-set-event.listener.spec.ts index 8f46a7084b0..098178548db 100644 --- a/feature-libs/product-configurator/rulebased/core/events/configurator-language-set-event.listener.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/events/configurator-language-set-event.listener.spec.ts @@ -1,16 +1,16 @@ import { TestBed } from '@angular/core/testing'; import { CxEvent, EventService, LanguageSetEvent } from '@spartacus/core'; import { Subject, Subscription } from 'rxjs'; -import createSpy = jasmine.createSpy; import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service'; import { Type } from '@angular/core'; import { ConfiguratorLanguageSetEventListener } from '@spartacus/product-configurator/rulebased'; +import { vi } from 'vitest'; const mockEventStream$ = new Subject(); class MockEventService implements Partial { - get = createSpy().and.returnValue(mockEventStream$.asObservable()); - dispatch = createSpy(); + get = vi.fn().mockReturnValue(mockEventStream$.asObservable()); + dispatch = vi.fn(); } class MockConfiguratorCommonsService { @@ -42,10 +42,7 @@ describe(`ConfiguratorLanguageSetEventListener`, () => { ConfiguratorCommonsService as Type ); - spyOn( - configuratorCommonsService, - 'removeProductBoundConfigurations' - ).and.callThrough(); + vi.spyOn(configuratorCommonsService, 'removeProductBoundConfigurations'); }); describe(`onLanguageSet`, () => { @@ -64,7 +61,7 @@ describe(`ConfiguratorLanguageSetEventListener`, () => { describe('ngOnDestroy', () => { it('should unsubscribe on ngOnDestroy', () => { - const spyUnsubscribe = spyOn(Subscription.prototype, 'unsubscribe'); + const spyUnsubscribe = vi.spyOn(Subscription.prototype, 'unsubscribe'); classUnderTest.ngOnDestroy(); expect(spyUnsubscribe).toHaveBeenCalled(); }); diff --git a/feature-libs/product-configurator/rulebased/core/events/configurator-logout-event.listener.spec.ts b/feature-libs/product-configurator/rulebased/core/events/configurator-logout-event.listener.spec.ts index 88a256ec303..fbcc745acde 100644 --- a/feature-libs/product-configurator/rulebased/core/events/configurator-logout-event.listener.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/events/configurator-logout-event.listener.spec.ts @@ -1,17 +1,17 @@ import { TestBed } from '@angular/core/testing'; import { CxEvent, EventService, LogoutEvent } from '@spartacus/core'; import { Subject, Subscription } from 'rxjs'; -import createSpy = jasmine.createSpy; import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service'; import { ConfiguratorExpertModeService } from '../services/configurator-expert-mode.service'; import { ConfiguratorLogoutEventListener } from './configurator-logout-event.listener'; import { Type } from '@angular/core'; +import { vi } from 'vitest'; const mockEventStream$ = new Subject(); class MockEventService implements Partial { - get = createSpy().and.returnValue(mockEventStream$.asObservable()); - dispatch = createSpy(); + get = vi.fn().mockReturnValue(mockEventStream$.asObservable()); + dispatch = vi.fn(); } class MockConfiguratorExpertModeService { @@ -54,20 +54,14 @@ describe(`ConfiguratorLogoutEventListener`, () => { configuratorExpertModeService = TestBed.inject( ConfiguratorExpertModeService as Type ); - spyOn( - configuratorExpertModeService, - 'setExpModeRequested' - ).and.callThrough(); - spyOn(configuratorExpertModeService, 'setExpModeActive').and.callThrough(); + vi.spyOn(configuratorExpertModeService, 'setExpModeRequested'); + vi.spyOn(configuratorExpertModeService, 'setExpModeActive'); configuratorCommonsService = TestBed.inject( ConfiguratorCommonsService as Type ); - spyOn( - configuratorCommonsService, - 'removeProductBoundConfigurations' - ).and.callThrough(); + vi.spyOn(configuratorCommonsService, 'removeProductBoundConfigurations'); }); describe(`onLogout`, () => { @@ -99,7 +93,7 @@ describe(`ConfiguratorLogoutEventListener`, () => { describe('ngOnDestroy', () => { it('should unsubscribe on ngOnDestroy', () => { - const spyUnsubscribe = spyOn(Subscription.prototype, 'unsubscribe'); + const spyUnsubscribe = vi.spyOn(Subscription.prototype, 'unsubscribe'); classUnderTest.ngOnDestroy(); expect(spyUnsubscribe).toHaveBeenCalled(); }); diff --git a/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts index 4dff00572cd..1073b5f3bd9 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/configurator-cart.service.spec.ts @@ -1,5 +1,4 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import * as ngrxStore from '@ngrx/store'; +import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; import { @@ -30,6 +29,7 @@ import { } from '../state/configurator-state'; import { getConfiguratorReducers } from '../state/reducers/index'; import { ConfiguratorCartService } from './configurator-cart.service'; +import { vi } from 'vitest'; let OWNER_CART_ENTRY = ConfiguratorModelUtils.createInitialOwner(); let OWNER_ORDER_ENTRY = ConfiguratorModelUtils.createInitialOwner(); @@ -101,7 +101,7 @@ describe('ConfiguratorCartService', () => { let store: Store; let configuratorUtils: CommonConfiguratorUtilsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { cartObs = of(cart); isStableObs = of(true); checkoutLoadingObs = of({ loading: true, error: false, data: undefined }); @@ -127,7 +127,7 @@ describe('ConfiguratorCartService', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { serviceUnderTest = TestBed.inject(ConfiguratorCartService); store = TestBed.inject(Store); @@ -152,6 +152,10 @@ describe('ConfiguratorCartService', () => { }; }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should create service', () => { expect(serviceUnderTest).toBeDefined(); }); @@ -163,10 +167,10 @@ describe('ConfiguratorCartService', () => { value: productConfiguration, }; - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderState) + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of(productConfigurationLoaderState) ); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); serviceUnderTest .readConfigurationForCartEntry(OWNER_CART_ENTRY) @@ -200,10 +204,11 @@ describe('ConfiguratorCartService', () => { }, }; - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderState) - ); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + // Apply all operators except select (index 0) + return of(productConfigurationLoaderState).pipe(..._ops.slice(1)); + }); + vi.spyOn(store, 'dispatch'); serviceUnderTest .readConfigurationForCartEntry(OWNER_CART_ENTRY) @@ -234,9 +239,9 @@ describe('ConfiguratorCartService', () => { }, }; - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderState) - ); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + return of(productConfigurationLoaderState).pipe(..._ops.slice(1)); + }); expect( serviceUnderTest.readConfigurationForCartEntry(OWNER_CART_ENTRY) @@ -262,9 +267,9 @@ describe('ConfiguratorCartService', () => { }, }; - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderState) - ); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + return of(productConfigurationLoaderState).pipe(..._ops.slice(1)); + }); expect( serviceUnderTest.readConfigurationForCartEntry(OWNER_CART_ENTRY) @@ -279,10 +284,10 @@ describe('ConfiguratorCartService', () => { value: productConfiguration, }; - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderState) + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of(productConfigurationLoaderState) ); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); serviceUnderTest .readConfigurationForOrderEntry(OWNER_ORDER_ENTRY) @@ -310,10 +315,10 @@ describe('ConfiguratorCartService', () => { }, }; - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderState) - ); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + return of(productConfigurationLoaderState).pipe(..._ops.slice(1)); + }); + vi.spyOn(store, 'dispatch'); serviceUnderTest .readConfigurationForOrderEntry(OWNER_ORDER_ENTRY) .subscribe() @@ -336,7 +341,7 @@ describe('ConfiguratorCartService', () => { owner: OWNER_PRODUCT, }; - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); serviceUnderTest.addToCart(PRODUCT_CODE, CONFIG_ID, OWNER_PRODUCT); @@ -355,7 +360,7 @@ describe('ConfiguratorCartService', () => { owner: OWNER_PRODUCT, }; - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); serviceUnderTest.addToCart(PRODUCT_CODE, CONFIG_ID, OWNER_PRODUCT, 100); @@ -374,9 +379,9 @@ describe('ConfiguratorCartService', () => { configuration: productConfiguration, }; - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); const obs = cold('|'); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); + vi.spyOn(store, 'pipe').mockReturnValueOnce(obs); serviceUnderTest.updateCartEntry(productConfiguration); expect(store.dispatch).toHaveBeenCalledWith( @@ -491,7 +496,7 @@ describe('ConfiguratorCartService', () => { describe('removeCartBoundConfigurations', () => { it('should fire respective action', () => { - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); serviceUnderTest.removeCartBoundConfigurations(); expect(store.dispatch).toHaveBeenCalledWith( diff --git a/feature-libs/product-configurator/rulebased/core/facade/configurator-commons.service.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/configurator-commons.service.spec.ts index 46ead4dd7b6..3cafb1c2e5a 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/configurator-commons.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/configurator-commons.service.spec.ts @@ -1,6 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; -import * as ngrxStore from '@ngrx/store'; +import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; import { StateUtils } from '@spartacus/core'; @@ -10,7 +9,7 @@ import { ConfiguratorModelUtils, } from '@spartacus/product-configurator/common'; import { cold } from 'jasmine-marbles'; -import { Observable, of } from 'rxjs'; +import { firstValueFrom, Observable, of } from 'rxjs'; import { productConfigurationWithConflicts } from '../../testing/configurator-test-data'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { Configurator } from '../model/configurator.model'; @@ -24,6 +23,7 @@ import { getConfiguratorReducers } from '../state/reducers/index'; import { ConfiguratorCartService } from './configurator-cart.service'; import { ConfiguratorCommonsService } from './configurator-commons.service'; import { ConfiguratorUtilsService } from './utils'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; let OWNER_PRODUCT = ConfiguratorModelUtils.createInitialOwner(); @@ -107,6 +107,9 @@ class MockconfiguratorUtilsService { return productConfiguration; } isConfigurationCreated(configuration: Configurator.Configuration): boolean { + if (!configuration) { + return false; + } const configId: String = configuration.configId; return configId !== undefined && configId.length !== 0; } @@ -131,7 +134,8 @@ class MockConfiguratorCartService { function callGetOrCreate( serviceUnderTest: ConfiguratorCommonsService, - owner: CommonConfigurator.Owner + owner: CommonConfigurator.Owner, + storeInstance: Store ) { const productConfigurationLoaderState: StateUtils.LoaderState = { @@ -145,7 +149,11 @@ function callGetOrCreate( x: productConfigurationLoaderState, y: productConfigurationLoaderStateChanged, }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); + // Mock store.pipe to inject our loader state observable while preserving the service operators (tap, filter, map) + vi.spyOn(storeInstance, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, tap, filter, map] = _ops; // [select, tap, filter, map] + return obs.pipe(tap, filter, map); + }); const configurationObs = serviceUnderTest.getOrCreateConfiguration(owner); return configurationObs; } @@ -164,7 +172,7 @@ describe('ConfiguratorCommonsService', () => { const cart: Cart = {}; cartObs = of(cart); - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({}), @@ -186,7 +194,7 @@ describe('ConfiguratorCommonsService', () => { }, ], }); - })); + }); beforeEach(() => { configOrderObservable = of(productConfiguration); configCartObservable = of(productConfiguration); @@ -239,11 +247,12 @@ describe('ConfiguratorCommonsService', () => { configuratorCartService = TestBed.inject( ConfiguratorCartService as Type ); - spyOn( - configuratorUtilsService, - 'createConfigurationExtract' - ).and.callThrough(); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(configuratorUtilsService, 'createConfigurationExtract'); + vi.spyOn(store, 'dispatch'); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it('should create service', () => { @@ -260,7 +269,7 @@ describe('ConfiguratorCommonsService', () => { }); it('should get pending changes from store', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => of(true)); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(true)); let hasPendingChanges = false; serviceUnderTest @@ -273,10 +282,7 @@ describe('ConfiguratorCommonsService', () => { describe('isConfigurationLoading', () => { it('should get configuration loading state from store', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => - of(configurationState.configurations.entities[OWNER_PRODUCT.key]) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(false)); let isLoading = false; serviceUnderTest @@ -287,14 +293,7 @@ describe('ConfiguratorCommonsService', () => { expect(isLoading).toBe(false); }); it('should get loading false in case loading attribute is not available in state', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => - of( - configurationStateWoLoading.configurations.entities[ - OWNER_PRODUCT.key - ] - ) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(false)); let isLoading = false; serviceUnderTest @@ -309,9 +308,7 @@ describe('ConfiguratorCommonsService', () => { it('should update a configuration, accessing the store', () => { cart.code = 'X'; cartObs = of(cart); - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); const changedAttribute: Configurator.Attribute = { name: ATTRIBUTE_NAME_1, groupId: GROUP_ID_1, @@ -357,9 +354,7 @@ describe('ConfiguratorCommonsService', () => { cart.code = undefined; cartObs = of(cart); isStableObservable = of(false); - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); const changedAttribute: Configurator.Attribute = { name: ATTRIBUTE_NAME_1, @@ -383,9 +378,7 @@ describe('ConfiguratorCommonsService', () => { it('should update a configuration in case if no updateType parameter in the call', () => { cart.code = 'X'; cartObs = of(cart); - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); const changedAttribute: Configurator.Attribute = { name: ATTRIBUTE_NAME_1, groupId: GROUP_ID_1, @@ -425,28 +418,22 @@ describe('ConfiguratorCommonsService', () => { value: configurationWithOverview, }; - it('should read OV by triggering respective action if that is not present', (done) => { + it('should read OV by triggering respective action if that is not present', async () => { expect(productConfiguration.overview).toBeUndefined(); - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => - of( - productConfigurationLoaderState, - productConfigurationLoaderStateLoading, - productConfigurationLoaderStateWithOv - ) + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of( + productConfigurationLoaderState, + productConfigurationLoaderStateLoading, + productConfigurationLoaderStateWithOv + ) ); - serviceUnderTest - .getConfigurationWithOverview(productConfiguration) - .subscribe(() => { - expect(store.dispatch).toHaveBeenCalledWith( - new ConfiguratorActions.GetConfigurationOverview( - productConfiguration - ) - ); - done(); - }) - .unsubscribe(); + await firstValueFrom( + serviceUnderTest.getConfigurationWithOverview(productConfiguration) + ); + expect(store.dispatch).toHaveBeenCalledWith( + new ConfiguratorActions.GetConfigurationOverview(productConfiguration) + ); }); describe('through filterNotLoadingAndCreatedConfiguration', () => { it('should not emit as long as loader state is `loading`', () => { @@ -463,31 +450,25 @@ describe('ConfiguratorCommonsService', () => { }); }); - it('should not dispatch an action if overview is already present', (done) => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderStateWithOv) + it('should not dispatch an action if overview is already present', async () => { + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of(productConfigurationLoaderStateWithOv) ); - serviceUnderTest - .getConfigurationWithOverview(productConfiguration) - .subscribe(() => { - expect(store.dispatch).toHaveBeenCalledTimes(0); - done(); - }) - .unsubscribe(); + await firstValueFrom( + serviceUnderTest.getConfigurationWithOverview(productConfiguration) + ); + expect(store.dispatch).toHaveBeenCalledTimes(0); }); - it('should return configuration with OV if that is already present in store', (done) => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationLoaderStateWithOv) + it('should return configuration with OV if that is already present in store', async () => { + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of(productConfigurationLoaderStateWithOv) ); - serviceUnderTest - .getConfigurationWithOverview(productConfiguration) - .subscribe((configuration) => { - expect(configuration).toBe(configurationWithOverview); - done(); - }) - .unsubscribe(); + const configuration = await firstValueFrom( + serviceUnderTest.getConfigurationWithOverview(productConfiguration) + ); + expect(configuration).toBe(configurationWithOverview); }); }); @@ -508,7 +489,11 @@ describe('ConfiguratorCommonsService', () => { x: productConfiguration, y: productConfigurationChanged, }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); + // Apply only the filter operator (select is bypassed but filter must run) + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, filter] = _ops; // [select, filter] + return obs.pipe(filter); + }); const configurationObs = serviceUnderTest.getConfiguration( productConfiguration.owner ); @@ -525,7 +510,10 @@ describe('ConfiguratorCommonsService', () => { x: productConfiguration, y: productConfigIncomplete, }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, filter] = _ops; // [select, filter] + return obs.pipe(filter); + }); const configurationObs = serviceUnderTest.getConfiguration( productConfiguration.owner @@ -540,7 +528,11 @@ describe('ConfiguratorCommonsService', () => { describe('getOrCreateConfiguration', () => { it('should return an unchanged observable of product configurations in case configurations exist and carry valid config IDs', () => { - const configurationObs = callGetOrCreate(serviceUnderTest, OWNER_PRODUCT); + const configurationObs = callGetOrCreate( + serviceUnderTest, + OWNER_PRODUCT, + store + ); expect(configurationObs).toBeObservable( cold('x-y', { x: productConfiguration, @@ -550,10 +542,7 @@ describe('ConfiguratorCommonsService', () => { }); it('should delegate to config cart service for cart bound configurations', () => { - spyOn( - configuratorCartService, - 'readConfigurationForCartEntry' - ).and.callThrough(); + vi.spyOn(configuratorCartService, 'readConfigurationForCartEntry'); serviceUnderTest.getOrCreateConfiguration(OWNER_CART_ENTRY); @@ -563,10 +552,7 @@ describe('ConfiguratorCommonsService', () => { }); it('should delegate to config cart service for order bound configurations', () => { - spyOn( - configuratorCartService, - 'readConfigurationForOrderEntry' - ).and.callThrough(); + vi.spyOn(configuratorCartService, 'readConfigurationForOrderEntry'); serviceUnderTest.getOrCreateConfiguration(OWNER_ORDER_ENTRY); @@ -580,12 +566,13 @@ describe('ConfiguratorCommonsService', () => { { loading: false, }; - - const obs = cold('x', { - x: productConfigurationLoaderState, + const obs = cold('x', { x: productConfigurationLoaderState }); + // Intercept the pipe call to inject our test observable while still running service operators + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + // Apply the service's operators (tap, filter, map) to our test observable + const [, tap, filter, map] = _ops; + return obs.pipe(tap, filter, map); }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); - const configurationObs = serviceUnderTest.getOrCreateConfiguration(OWNER_PRODUCT); expect(configurationObs).toBeObservable(cold('', {})); @@ -602,12 +589,11 @@ describe('ConfiguratorCommonsService', () => { { loading: false, }; - - const obs = cold('x', { - x: productConfigurationLoaderState, + const obs = cold('x', { x: productConfigurationLoaderState }); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, tap, filter, map] = _ops; + return obs.pipe(tap, filter, map); }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); - const configurationObs = serviceUnderTest.getOrCreateConfiguration( OWNER_PRODUCT, CONFIG_ID_TEMPLATE @@ -626,12 +612,11 @@ describe('ConfiguratorCommonsService', () => { { loading: true, }; - - const obs = cold('x', { - x: productConfigurationLoaderState, + const obs = cold('x', { x: productConfigurationLoaderState }); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, tap, filter, map] = _ops; + return obs.pipe(tap, filter, map); }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); - const configurationObs = serviceUnderTest.getOrCreateConfiguration(OWNER_PRODUCT); expect(configurationObs).toBeObservable(cold('', {})); @@ -644,12 +629,11 @@ describe('ConfiguratorCommonsService', () => { loading: false, error: true, }; - - const obs = cold('x', { - x: productConfigurationLoaderState, + const obs = cold('x', { x: productConfigurationLoaderState }); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, tap, filter, map] = _ops; + return obs.pipe(tap, filter, map); }); - spyOnProperty(ngrxStore, 'select').and.returnValue(() => () => obs); - const configurationObs = serviceUnderTest.getOrCreateConfiguration(OWNER_PRODUCT); expect(configurationObs).toBeObservable(cold('', {})); @@ -658,32 +642,22 @@ describe('ConfiguratorCommonsService', () => { }); describe('hasConflicts', () => { - it('should return false in case of no conflicts', (done) => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) + it('should return false in case of no conflicts', async () => { + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); + const hasConflicts = await firstValueFrom( + serviceUnderTest.hasConflicts(OWNER_PRODUCT) ); - serviceUnderTest - .hasConflicts(OWNER_PRODUCT) - .pipe() - .subscribe((hasConflicts) => { - expect(hasConflicts).toBe(false); - done(); - }) - .unsubscribe(); + expect(hasConflicts).toBe(false); }); - it('should return true in case of conflicts', (done) => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfigurationWithConflicts) + it('should return true in case of conflicts', async () => { + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of(productConfigurationWithConflicts) ); - serviceUnderTest - .hasConflicts(OWNER_PRODUCT) - .pipe() - .subscribe((hasConflicts) => { - expect(hasConflicts).toBe(true); - done(); - }) - .unsubscribe(); + const hasConflicts = await firstValueFrom( + serviceUnderTest.hasConflicts(OWNER_PRODUCT) + ); + expect(hasConflicts).toBe(true); }); }); @@ -738,9 +712,7 @@ describe('ConfiguratorCommonsService', () => { describe('readAttributeDomain', () => { it('should read attribute domain with attribute key', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); serviceUnderTest.readAttributeDomain( productConfiguration.owner, group1, @@ -755,9 +727,7 @@ describe('ConfiguratorCommonsService', () => { ); }); it('should read attribute domain with attribute name if key is not available', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); serviceUnderTest.readAttributeDomain( productConfiguration.owner, group1, diff --git a/feature-libs/product-configurator/rulebased/core/facade/configurator-group-status.service.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/configurator-group-status.service.spec.ts index fce670da813..4d8062e9b3d 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/configurator-group-status.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/configurator-group-status.service.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { of } from 'rxjs'; import { @@ -17,17 +17,18 @@ import { ConfiguratorActions } from '../state/actions/index'; import { StateWithConfigurator } from '../state/configurator-state'; import { ConfiguratorGroupStatusService } from './configurator-group-status.service'; import { ConfiguratorUtilsService } from './utils/configurator-utils.service'; +import { vi } from 'vitest'; describe('ConfiguratorGroupStatusService', () => { let classUnderTest: ConfiguratorGroupStatusService; let store: Store; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreModule.forRoot({})], providers: [ConfiguratorUtilsService, ConfiguratorGroupStatusService], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( @@ -35,8 +36,8 @@ describe('ConfiguratorGroupStatusService', () => { ); store = TestBed.inject(Store as Type>); - spyOn(store, 'dispatch').and.stub(); - spyOn(store, 'pipe').and.returnValue(of(productConfiguration)); + vi.spyOn(store, 'dispatch').mockImplementation(() => {}); + vi.spyOn(store, 'pipe').mockReturnValue(of(productConfiguration)); }); it('should be created', () => { @@ -59,7 +60,7 @@ describe('ConfiguratorGroupStatusService', () => { }); it('should get parent group, when all subgroups are visited', () => { - spyOn(store, 'select').and.returnValue(of(true)); + vi.spyOn(store, 'select').mockReturnValue(of(true)); classUnderTest.setGroupStatusVisited(productConfiguration, GROUP_ID_4); const expectedAction = new ConfiguratorActions.SetGroupsVisited({ @@ -72,7 +73,7 @@ describe('ConfiguratorGroupStatusService', () => { it('should not get parent group, when not all subgroups are visited', () => { //Not all subgroups are visited - spyOn(store, 'select').and.returnValue(of(false)); + vi.spyOn(store, 'select').mockReturnValue(of(false)); classUnderTest.setGroupStatusVisited(productConfiguration, GROUP_ID_6); @@ -85,7 +86,7 @@ describe('ConfiguratorGroupStatusService', () => { }); it('should get all parent groups, when lowest subgroup are visited', () => { - spyOn(store, 'select').and.returnValue(of(true)); + vi.spyOn(store, 'select').mockReturnValue(of(true)); classUnderTest.setGroupStatusVisited(productConfiguration, GROUP_ID_8); diff --git a/feature-libs/product-configurator/rulebased/core/facade/configurator-groups.service.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/configurator-groups.service.spec.ts index 2f88c0f86da..3cdde562ed0 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/configurator-groups.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/configurator-groups.service.spec.ts @@ -1,9 +1,9 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { ActiveCartFacade } from '@spartacus/cart/base/root'; import { ConfiguratorModelUtils } from '@spartacus/product-configurator/common'; -import { Observable, of } from 'rxjs'; +import { firstValueFrom, Observable, of } from 'rxjs'; import { CONFIG_ID, GROUP_ID_1, @@ -25,6 +25,7 @@ import { ConfiguratorCommonsService } from './configurator-commons.service'; import { ConfiguratorGroupStatusService } from './configurator-group-status.service'; import { ConfiguratorGroupsService } from './configurator-groups.service'; import { ConfiguratorUtilsService } from './utils/configurator-utils.service'; +import { vi } from 'vitest'; const PRODUCT_CONFIG_CURRENT_GROUP_IS_CONFLICT: Configurator.Configuration = { ...productConfigurationWithConflicts, @@ -49,7 +50,7 @@ describe('ConfiguratorGroupsService', () => { let configGroupStatusService: ConfiguratorGroupStatusService; let configFacadeUtilsService: ConfiguratorUtilsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreModule.forRoot({})], providers: [ @@ -67,7 +68,7 @@ describe('ConfiguratorGroupsService', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( ConfiguratorGroupsService as Type @@ -83,26 +84,26 @@ describe('ConfiguratorGroupsService', () => { ConfiguratorUtilsService as Type ); - spyOn(store, 'dispatch').and.stub(); - spyOn(store, 'pipe').and.returnValue(of(productConfiguration)); + vi.spyOn(store, 'dispatch').mockImplementation(() => {}); + vi.spyOn(store, 'pipe').mockReturnValue(of(productConfiguration)); - spyOn(configGroupStatusService, 'setGroupStatusVisited').and.callThrough(); - spyOn(configGroupStatusService, 'isGroupVisited').and.callThrough(); - spyOn(configFacadeUtilsService, 'getParentGroup').and.callThrough(); - spyOn(configFacadeUtilsService, 'hasSubGroups').and.callThrough(); - spyOn(configFacadeUtilsService, 'getGroupById').and.callThrough(); + vi.spyOn(configGroupStatusService, 'setGroupStatusVisited'); + vi.spyOn(configGroupStatusService, 'isGroupVisited'); + vi.spyOn(configFacadeUtilsService, 'getParentGroup'); + vi.spyOn(configFacadeUtilsService, 'hasSubGroups'); + vi.spyOn(configFacadeUtilsService, 'getGroupById'); }); it('should create service', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); expect(classUnderTest).toBeDefined(); }); describe('getCurrentGroupId', () => { - it('should return a current group ID from state', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return a current group ID from state', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const currentGroup = classUnderTest.getCurrentGroupId( @@ -110,14 +111,12 @@ describe('ConfiguratorGroupsService', () => { ); expect(currentGroup).toBeDefined(); - currentGroup.subscribe((groupId) => { - expect(groupId).toBe(GROUP_ID_2); - done(); - }); + const groupId = await firstValueFrom(currentGroup); + expect(groupId).toBe(GROUP_ID_2); }); - it('should return a current group ID from configuration', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return a current group ID from configuration', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of({ ...productConfiguration, interactionState: { currentGroup: null }, @@ -128,36 +127,32 @@ describe('ConfiguratorGroupsService', () => { ); expect(currentGroup).toBeDefined(); - currentGroup.subscribe((groupId) => { - expect(groupId).toBe(GROUP_ID_1); - done(); - }); + const groupId = await firstValueFrom(currentGroup); + expect(groupId).toBe(GROUP_ID_1); }); - it('should return undefined if no group exist', (done) => { + it('should return undefined if no group exist', async () => { const configNoGroups: Configurator.Configuration = { ...ConfiguratorTestUtils.createConfiguration( 'abc', ConfiguratorModelUtils.createInitialOwner() ), }; - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configNoGroups) ); const currentGroupId = classUnderTest.getCurrentGroupId( productConfiguration.owner ); - currentGroupId.subscribe((groupId) => { - expect(groupId).toBeUndefined(); - done(); - }); + const groupId = await firstValueFrom(currentGroupId); + expect(groupId).toBeUndefined(); }); }); describe('getMenuParentGroup', () => { - it('should get the parentGroup from uiState', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should get the parentGroup from uiState', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const parentGroup = classUnderTest.getMenuParentGroup( @@ -165,19 +160,17 @@ describe('ConfiguratorGroupsService', () => { ); expect(parentGroup).toBeDefined(); - parentGroup.subscribe((group) => { - expect(group).toBe(productConfiguration.groups[2]); - done(); - }); + const group = await firstValueFrom(parentGroup); + expect(group).toBe(productConfiguration.groups[2]); }); - it('should return undefined if menu parent group is not availaible in uiState', (done) => { + it('should return undefined if menu parent group is not availaible in uiState', async () => { const configurationWoMenuParentGroup = ConfiguratorTestUtils.createConfiguration( CONFIG_ID, ConfiguratorModelUtils.createInitialOwner() ); - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configurationWoMenuParentGroup) ); const parentGroup = classUnderTest.getMenuParentGroup( @@ -185,13 +178,11 @@ describe('ConfiguratorGroupsService', () => { ); expect(parentGroup).toBeDefined(); - parentGroup.subscribe((group) => { - expect(group).toBeUndefined(); - done(); - }); + const group = await firstValueFrom(parentGroup); + expect(group).toBeUndefined(); }); - it('should return undefined if menu parent group cannot be found', (done) => { + it('should return undefined if menu parent group cannot be found', async () => { const configurationWoMenuParentGroup: Configurator.Configuration = { ...ConfiguratorTestUtils.createConfiguration( CONFIG_ID, @@ -201,7 +192,7 @@ describe('ConfiguratorGroupsService', () => { menuParentGroup: 'Conflict header group that is gone', }, }; - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configurationWoMenuParentGroup) ); const parentGroup = classUnderTest.getMenuParentGroup( @@ -209,16 +200,14 @@ describe('ConfiguratorGroupsService', () => { ); expect(parentGroup).toBeDefined(); - parentGroup.subscribe((group) => { - expect(group).toBeUndefined(); - done(); - }); + const group = await firstValueFrom(parentGroup); + expect(group).toBeUndefined(); }); }); describe('getNextGroupId', () => { - it('should return a next group', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return a next group', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const currentGroup = classUnderTest.getNextGroupId( @@ -226,10 +215,8 @@ describe('ConfiguratorGroupsService', () => { ); expect(currentGroup).toBeDefined(); - currentGroup.subscribe((groupId) => { - expect(groupId).toBe(GROUP_ID_4); - done(); - }); + const groupId = await firstValueFrom(currentGroup); + expect(groupId).toBe(GROUP_ID_4); }); }); @@ -254,22 +241,20 @@ describe('ConfiguratorGroupsService', () => { }); describe('getNextGroupDescription', () => { - it('should return description of next group', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return description of next group', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const nextGroupDescription = classUnderTest.getNextGroupDescription(productConfiguration); expect(nextGroupDescription).toBeDefined(); - nextGroupDescription.subscribe((description) => { - expect(description).toBe(DESCRIPTION_FOR + GROUP_ID_4); - done(); - }); + const description = await firstValueFrom(nextGroupDescription); + expect(description).toBe(DESCRIPTION_FOR + GROUP_ID_4); }); - it('should return empty string if no next group exists', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return empty string if no next group exists', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); productConfiguration.interactionState.currentGroup = GROUP_ID_10; @@ -277,17 +262,15 @@ describe('ConfiguratorGroupsService', () => { classUnderTest.getNextGroupDescription(productConfiguration); expect(nextGroupDescription).toBeDefined(); - nextGroupDescription.subscribe((description) => { - expect(description).toBe(''); - done(); - }); + const description = await firstValueFrom(nextGroupDescription); + expect(description).toBe(''); productConfiguration.interactionState.currentGroup = GROUP_ID_2; }); }); describe('getPreviousGroupId', () => { - it('should return a previous group ID', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return a previous group ID', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const currentGroup = classUnderTest.getPreviousGroupId( @@ -295,19 +278,17 @@ describe('ConfiguratorGroupsService', () => { ); expect(currentGroup).toBeDefined(); - currentGroup.subscribe((groupId) => { - expect(groupId).toBe(GROUP_ID_1); - done(); - }); + const groupId = await firstValueFrom(currentGroup); + expect(groupId).toBe(GROUP_ID_1); }); - it('should return null in case configuration is in immediate conflict resolution and previous group is a conflict one', (done) => { + it('should return null in case configuration is in immediate conflict resolution and previous group is a conflict one', async () => { let configurationWithConflicts = structuredClone( productConfigurationWithConflicts ); configurationWithConflicts.immediateConflictResolution = true; - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configurationWithConflicts) ); const currentGroup = classUnderTest.getPreviousGroupId( @@ -315,13 +296,11 @@ describe('ConfiguratorGroupsService', () => { ); expect(currentGroup).toBeDefined(); - currentGroup.subscribe((groupId) => { - expect(groupId).toBeUndefined(); - done(); - }); + const groupId = await firstValueFrom(currentGroup); + expect(groupId).toBeUndefined(); }); - it('should return a previous group ID in case configuration is in immediate conflict resolution and previous group not is a conflict one', (done) => { + it('should return a previous group ID in case configuration is in immediate conflict resolution and previous group not is a conflict one', async () => { let configurationWithConflicts = structuredClone( productConfigurationWithConflicts ); @@ -329,7 +308,7 @@ describe('ConfiguratorGroupsService', () => { configurationWithConflicts.interactionState.currentGroup = GROUP_ID_2; configurationWithConflicts.interactionState.menuParentGroup = GROUP_ID_3; - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configurationWithConflicts) ); const currentGroup = classUnderTest.getPreviousGroupId( @@ -337,30 +316,26 @@ describe('ConfiguratorGroupsService', () => { ); expect(currentGroup).toBeDefined(); - currentGroup.subscribe((groupId) => { - expect(groupId).toBe(GROUP_ID_1); - done(); - }); + const groupId = await firstValueFrom(currentGroup); + expect(groupId).toBe(GROUP_ID_1); }); }); describe('getPreviousGroupDescription', () => { - it('should return description of previous group', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return description of previous group', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const previousGroupDescription = classUnderTest.getPreviousGroupDescription(productConfiguration); expect(previousGroupDescription).toBeDefined(); - previousGroupDescription.subscribe((description) => { - expect(description).toBe(DESCRIPTION_FOR + GROUP_ID_1); - done(); - }); + const description = await firstValueFrom(previousGroupDescription); + expect(description).toBe(DESCRIPTION_FOR + GROUP_ID_1); }); - it('should return empty string if no previous group exists', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should return empty string if no previous group exists', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); productConfiguration.interactionState.currentGroup = GROUP_ID_1; @@ -368,17 +343,15 @@ describe('ConfiguratorGroupsService', () => { classUnderTest.getPreviousGroupDescription(productConfiguration); expect(previousGroupDescription).toBeDefined(); - previousGroupDescription.subscribe((description) => { - expect(description).toBe(''); - done(); - }); + const description = await firstValueFrom(previousGroupDescription); + expect(description).toBe(''); productConfiguration.interactionState.currentGroup = GROUP_ID_2; }); }); describe('setGroupStatusVisited', () => { it('should call setGroupStatusVisited of groupStatusService', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); classUnderTest.setGroupStatusVisited( @@ -391,7 +364,7 @@ describe('ConfiguratorGroupsService', () => { }); it('should delegate setting the parent group to the store', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); classUnderTest.setMenuParentGroup(productConfiguration.owner, GROUP_ID_1); @@ -403,7 +376,7 @@ describe('ConfiguratorGroupsService', () => { }); it('should call group status in navigate to different group', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); classUnderTest.navigateToGroup( @@ -437,7 +410,7 @@ describe('ConfiguratorGroupsService', () => { describe('navigateToConflictSolver', () => { it('should trigger change group action in case conflict group deviates from current one', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfigurationWithConflicts) ); classUnderTest.navigateToConflictSolver( @@ -454,7 +427,7 @@ describe('ConfiguratorGroupsService', () => { ); }); it('should also trigger change group action in case current group is already the first conflict group because group menu component relies on interactionState.issueNavigationDone', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(PRODUCT_CONFIG_CURRENT_GROUP_IS_CONFLICT) ); classUnderTest.navigateToConflictSolver( @@ -473,7 +446,7 @@ describe('ConfiguratorGroupsService', () => { it('should not navigate in case no conflict group is present', () => { const consistentConfiguration = ConfiguratorTestUtils.createConfiguration('1'); - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(consistentConfiguration) ); classUnderTest.navigateToConflictSolver(consistentConfiguration.owner); @@ -483,7 +456,7 @@ describe('ConfiguratorGroupsService', () => { describe('navigateToFirstIncompleteGroup', () => { it('should go to first incomplete group', () => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); classUnderTest.navigateToFirstIncompleteGroup(productConfiguration.owner); @@ -500,7 +473,7 @@ describe('ConfiguratorGroupsService', () => { it('should not navigate in case no incomplete group is present', () => { const completeConfiguration = ConfiguratorTestUtils.createConfiguration('1'); - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(completeConfiguration) ); classUnderTest.navigateToFirstIncompleteGroup(productConfiguration.owner); @@ -543,8 +516,8 @@ describe('ConfiguratorGroupsService', () => { }); describe('getConflictGroupForImmediateConflictResolution', () => { - it('should not return any conflict group because showConflictSolverDialog is not defined', (done) => { - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + it('should not return any conflict group because showConflictSolverDialog is not defined', async () => { + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(productConfiguration) ); const conflictGroups = @@ -553,19 +526,17 @@ describe('ConfiguratorGroupsService', () => { ); expect(conflictGroups).toBeDefined(); - conflictGroups.subscribe((group) => { - expect(group).toBeUndefined(); - done(); - }); + const group = await firstValueFrom(conflictGroups); + expect(group).toBeUndefined(); }); - it('should not return any conflict group because showConflictSolverDialog is set to false', (done) => { + it('should not return any conflict group because showConflictSolverDialog is set to false', async () => { let configurationWithConflicts = structuredClone( productConfigurationWithConflicts ); configurationWithConflicts.interactionState.showConflictSolverDialog = false; - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configurationWithConflicts) ); const conflictGroups = @@ -574,19 +545,17 @@ describe('ConfiguratorGroupsService', () => { ); expect(conflictGroups).toBeDefined(); - conflictGroups.subscribe((group) => { - expect(group).toBeUndefined(); - done(); - }); + const group = await firstValueFrom(conflictGroups); + expect(group).toBeUndefined(); }); - it('should return a conflict group', (done) => { + it('should return a conflict group', async () => { let configurationWithConflicts = structuredClone( productConfigurationWithConflicts ); configurationWithConflicts.interactionState.showConflictSolverDialog = true; - spyOn(configuratorCommonsService, 'getConfiguration').and.returnValue( + vi.spyOn(configuratorCommonsService, 'getConfiguration').mockReturnValue( of(configurationWithConflicts) ); const conflictGroups = @@ -595,11 +564,9 @@ describe('ConfiguratorGroupsService', () => { ); expect(conflictGroups).toBeDefined(); - conflictGroups.subscribe((group) => { - expect(group).not.toBeUndefined(); - expect(group?.id).toEqual(GROUP_ID_CONFLICT_3); - done(); - }); + const group = await firstValueFrom(conflictGroups); + expect(group).not.toBeUndefined(); + expect(group?.id).toEqual(GROUP_ID_CONFLICT_3); }); }); diff --git a/feature-libs/product-configurator/rulebased/core/facade/routing/configurator-router.listener.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/routing/configurator-router.listener.spec.ts index decffb0b6b0..b5dae6b31e8 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/routing/configurator-router.listener.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/routing/configurator-router.listener.spec.ts @@ -1,10 +1,11 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { RouterState, RoutingService } from '@spartacus/core'; import { Observable, Subscription, of } from 'rxjs'; import { ConfiguratorCartService } from '../configurator-cart.service'; import { ConfiguratorRouterListener } from './configurator-router.listener'; import { ConfiguratorQuantityService } from '../../services/configurator-quantity.service'; +import { vi } from 'vitest'; const QUANTITY = 99; class MockConfiguratorCartService { @@ -64,7 +65,7 @@ describe('ConfiguratorRouterListener', () => { let configuratorCartService: ConfiguratorCartService; let configuratorQuantityService: ConfiguratorQuantityService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ { @@ -81,7 +82,7 @@ describe('ConfiguratorRouterListener', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { configuratorCartService = TestBed.inject( ConfiguratorCartService as Type @@ -90,11 +91,12 @@ describe('ConfiguratorRouterListener', () => { ConfiguratorQuantityService as Type ); - spyOn( - configuratorCartService, - 'removeCartBoundConfigurations' - ).and.callThrough(); - spyOn(configuratorQuantityService, 'setQuantity').and.callThrough(); + vi.spyOn(configuratorCartService, 'removeCartBoundConfigurations'); + vi.spyOn(configuratorQuantityService, 'setQuantity'); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); describe('observeRouterChanges', () => { @@ -142,7 +144,7 @@ describe('ConfiguratorRouterListener', () => { const classUnderTest = TestBed.inject( ConfiguratorRouterListener as Type ); - const spyUnsubscribe = spyOn(Subscription.prototype, 'unsubscribe'); + const spyUnsubscribe = vi.spyOn(Subscription.prototype, 'unsubscribe'); classUnderTest.ngOnDestroy(); expect(spyUnsubscribe).toHaveBeenCalled(); }); diff --git a/feature-libs/product-configurator/rulebased/core/facade/utils/configurator-utils.service.spec.ts b/feature-libs/product-configurator/rulebased/core/facade/utils/configurator-utils.service.spec.ts index 25abf2908e4..8e6bd53ad8a 100644 --- a/feature-libs/product-configurator/rulebased/core/facade/utils/configurator-utils.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/facade/utils/configurator-utils.service.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { StateUtils } from '@spartacus/core'; import { ConfiguratorModelUtils } from '@spartacus/product-configurator/common'; import { @@ -117,11 +117,11 @@ function mergeChangesAndGetFirstGroup( describe('ConfiguratorUtilsService', () => { let classUnderTest: ConfiguratorUtilsService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ConfiguratorUtilsService], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( diff --git a/feature-libs/product-configurator/rulebased/core/services/configurator-expert-mode.service.spec.ts b/feature-libs/product-configurator/rulebased/core/services/configurator-expert-mode.service.spec.ts index fc8fd1c4cfa..b8b9877769c 100644 --- a/feature-libs/product-configurator/rulebased/core/services/configurator-expert-mode.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/services/configurator-expert-mode.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { ConfiguratorExpertModeService } from './configurator-expert-mode.service'; @@ -24,16 +25,11 @@ describe('ConfiguratorExpertModeService', () => { expect(result).toBeUndefined(); }); - it('should return value that was set with setExpModeRequested', (done) => { + it('should return value that was set with setExpModeRequested', async () => { const expMode = true; classUnderTest.setExpModeRequested(expMode); - classUnderTest - .getExpModeRequested() - .pipe(take(1)) - .subscribe((userId) => { - expect(userId).toBe(expMode); - done(); - }); + const userId = await firstValueFrom(classUnderTest.getExpModeRequested()); + expect(userId).toBe(expMode); }); }); @@ -49,16 +45,11 @@ describe('ConfiguratorExpertModeService', () => { expect(result).toBeUndefined(); }); - it('should return value that was set with setExpModeActive', (done) => { + it('should return value that was set with setExpModeActive', async () => { const expMode = true; classUnderTest.setExpModeActive(expMode); - classUnderTest - .getExpModeActive() - .pipe(take(1)) - .subscribe((userId) => { - expect(userId).toBe(expMode); - done(); - }); + const userId = await firstValueFrom(classUnderTest.getExpModeActive()); + expect(userId).toBe(expMode); }); }); }); diff --git a/feature-libs/product-configurator/rulebased/core/services/configurator-quantity.service.spec.ts b/feature-libs/product-configurator/rulebased/core/services/configurator-quantity.service.spec.ts index dc23ebf7484..3f8eb665350 100644 --- a/feature-libs/product-configurator/rulebased/core/services/configurator-quantity.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/services/configurator-quantity.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { ConfiguratorQuantityService } from './configurator-quantity.service'; import { cold } from 'jasmine-marbles'; @@ -18,16 +19,11 @@ describe('ConfiguratorQuantityService', () => { expect(classUnderTest.getQuantity()).toBeObservable(cold('')); }); - it('should return value that was set with setQuantity', (done) => { + it('should return value that was set with setQuantity', async () => { const result = 100; classUnderTest.setQuantity(result); - classUnderTest - .getQuantity() - .pipe(take(1)) - .subscribe((quantity) => { - expect(quantity).toBe(result); - done(); - }); + const quantity = await firstValueFrom(classUnderTest.getQuantity()); + expect(quantity).toBe(result); }); }); }); diff --git a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic-effect.service.spec.ts b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic-effect.service.spec.ts index 955231be34d..8fac13b069d 100644 --- a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic-effect.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic-effect.service.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ConfiguratorModelUtils } from '@spartacus/product-configurator/common'; import { ATTRIBUTE_1_CHECKBOX, @@ -132,11 +132,11 @@ const groupListWithConflictsAndAttributesOnRootLevel: Configurator.Group[] = [ describe('ConfiguratorBasicEffectService', () => { let classUnderTest: ConfiguratorBasicEffectService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ConfiguratorBasicEffectService], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( ConfiguratorBasicEffectService as Type diff --git a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic.effect.spec.ts b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic.effect.spec.ts index f85da3a88c3..f85bb5a007d 100644 --- a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic.effect.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-basic.effect.spec.ts @@ -7,7 +7,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import * as ngrxStore from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store'; import { LoggerService, tryNormalizeHttpError } from '@spartacus/core'; import { @@ -35,6 +34,7 @@ import { import { getConfiguratorReducers } from './../reducers/index'; import { ConfiguratorBasicEffectService } from './configurator-basic-effect.service'; import * as fromEffects from './configurator-basic.effect'; +import { vi } from 'vitest'; const productCode = 'CONF_LAPTOP'; const configId = '1234-56-7890'; @@ -180,12 +180,12 @@ class MockLoggerService { } describe('ConfiguratorEffect', () => { - let createMock: jasmine.Spy; - let readMock: jasmine.Spy; - let updateConfigurationMock: jasmine.Spy; - let readPriceSummaryMock: jasmine.Spy; - let overviewMock: jasmine.Spy; - let updateOverviewMock: jasmine.Spy; + let createMock: vi.Mock; + let readMock: vi.Mock; + let updateConfigurationMock: vi.Mock; + let readPriceSummaryMock: vi.Mock; + let overviewMock: vi.Mock; + let updateOverviewMock: vi.Mock; let configEffects: fromEffects.ConfiguratorBasicEffects; let configuratorBasicEffectService: ConfiguratorBasicEffectService; @@ -194,20 +194,14 @@ describe('ConfiguratorEffect', () => { let actions$: Observable; beforeEach(() => { - createMock = jasmine.createSpy().and.returnValue(of(productConfiguration)); - updateConfigurationMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration)); - readPriceSummaryMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration)); - readMock = jasmine.createSpy().and.returnValue(of(productConfiguration)); - overviewMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration.overview)); - updateOverviewMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration.overview)); + createMock = vi.fn().mockReturnValue(of(productConfiguration)); + updateConfigurationMock = vi.fn().mockReturnValue(of(productConfiguration)); + readPriceSummaryMock = vi.fn().mockReturnValue(of(productConfiguration)); + readMock = vi.fn().mockReturnValue(of(productConfiguration)); + overviewMock = vi.fn().mockReturnValue(of(productConfiguration.overview)); + updateOverviewMock = vi + .fn() + .mockReturnValue(of(productConfiguration.overview)); class MockConnector { createConfiguration = createMock; @@ -249,6 +243,10 @@ describe('ConfiguratorEffect', () => { store = TestBed.inject(Store as Type>); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should provide configuration effects', () => { expect(configEffects).toBeTruthy(); }); @@ -331,7 +329,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - createMock.and.returnValue(throwError(() => errorResponse)); + createMock.mockReturnValue(throwError(() => errorResponse)); const action = new ConfiguratorActions.CreateConfiguration({ owner: productConfiguration.owner, @@ -373,11 +371,11 @@ describe('ConfiguratorEffect', () => { const cachedConfiguration: Configurator.Configuration = { ...ConfiguratorTestUtils.createConfiguration(configId, cpqOwner), }; - spyOn( + vi.spyOn( configuratorBasicEffectService, 'getConfigurationIfTabAlreadyLoaded' - ).and.returnValue(cachedConfiguration); - readMock.calls.reset(); + ).mockReturnValue(cachedConfiguration); + readMock.mockClear(); const action = new ConfiguratorActions.ReadConfiguration({ configuration: { @@ -396,10 +394,10 @@ describe('ConfiguratorEffect', () => { }); it('should not consult the store cache for non-CPQ configurator types', () => { - const cacheSpy = spyOn( + const cacheSpy = vi.spyOn( configuratorBasicEffectService, 'getConfigurationIfTabAlreadyLoaded' - ).and.callThrough(); + ); const action = new ConfiguratorActions.ReadConfiguration({ configuration: { @@ -419,7 +417,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case connector raises an error', () => { - readMock.and.returnValue(throwError(() => errorResponse)); + readMock.mockReturnValue(throwError(() => errorResponse)); const action = new ConfiguratorActions.ReadConfiguration({ configuration: productConfiguration, groupId: '', @@ -474,7 +472,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case connector raises an error', () => { - readMock.and.returnValue(throwError(() => errorResponse)); + readMock.mockReturnValue(throwError(() => errorResponse)); const readConfigurationFailAction = new ConfiguratorActions.ReadConfigurationFail({ @@ -522,7 +520,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - overviewMock.and.returnValue(throwError(() => errorResponse)); + overviewMock.mockReturnValue(throwError(() => errorResponse)); const overviewAction = new ConfiguratorActions.GetConfigurationOverview( productConfiguration ); @@ -562,7 +560,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - updateOverviewMock.and.returnValue(throwError(() => errorResponse)); + updateOverviewMock.mockReturnValue(throwError(() => errorResponse)); const overviewAction = new ConfiguratorActions.UpdateConfigurationOverview( productConfiguration @@ -604,7 +602,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - updateConfigurationMock.and.returnValue(throwError(() => errorResponse)); + updateConfigurationMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput = productConfiguration; const action = new ConfiguratorActions.UpdateConfiguration(payloadInput); @@ -622,7 +620,7 @@ describe('ConfiguratorEffect', () => { // Give the connector some virtual "processing time" so that overlapping vs. // sequential handling becomes observable on the marble time line. The same cold // observable is replayed relative to each (sequential) subscription. - updateConfigurationMock.and.returnValue( + updateConfigurationMock.mockReturnValue( cold('--(c|)', { c: productConfiguration }) ); const action = new ConfiguratorActions.UpdateConfiguration( @@ -662,7 +660,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - readPriceSummaryMock.and.returnValue(throwError(() => errorResponse)); + readPriceSummaryMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput = productConfiguration; const updatePriceSummaryAction = new ConfiguratorActions.UpdatePriceSummary(payloadInput); @@ -950,7 +948,7 @@ describe('ConfiguratorEffect', () => { }); it('should emit ReadConfigurationFail in case read call is not successful', () => { - readMock.and.returnValue(throwError(() => errorResponse)); + readMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput: Configurator.Configuration = { ...ConfiguratorTestUtils.createConfiguration(configId, owner), productCode: productCode, @@ -989,13 +987,17 @@ describe('ConfiguratorEffect', () => { }); it('should emit remove configuration action for configurations that are purely product bound', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(configurationState) - ); - entitiesInConfigurationState[productConfiguration.owner.key] = productConfiguration.owner.key; + vi.spyOn(store, 'pipe').mockReturnValueOnce( + of( + new ConfiguratorActions.RemoveConfiguration({ + ownerKey: [productConfiguration.owner.key], + }) + ) + ); + const removeProductBoundConfigurationsAction = new ConfiguratorActions.RemoveProductBoundConfigurations(); diff --git a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-cart.effect.spec.ts b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-cart.effect.spec.ts index d510618f4b2..a0c96f4c201 100644 --- a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-cart.effect.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-cart.effect.spec.ts @@ -7,8 +7,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import * as ngrxStore from '@ngrx/store'; -import { StoreModule } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; import { CartActions } from '@spartacus/cart/base/core'; import { CartModification } from '@spartacus/cart/base/root'; import { LoggerService, tryNormalizeHttpError } from '@spartacus/core'; @@ -25,9 +24,13 @@ import { RulebasedConfiguratorConnector } from '../../connectors/rulebased-confi import { ConfiguratorUtilsService } from '../../facade/utils/configurator-utils.service'; import { Configurator } from '../../model/configurator.model'; import { ConfiguratorActions } from '../actions/index'; -import { CONFIGURATOR_FEATURE } from '../configurator-state'; +import { + CONFIGURATOR_FEATURE, + StateWithConfigurator, +} from '../configurator-state'; import { getConfiguratorReducers } from './../reducers/index'; import * as fromEffects from './configurator-cart.effect'; +import { vi } from 'vitest'; const productCode = 'CONF_LAPTOP'; const configId = '1234-56-7890'; @@ -132,10 +135,6 @@ const cartModification: CartModification = { }; const cartModificationWithoutEntry: CartModification = {}; -let entitiesInConfigurationState: { - [id: string]: any; -} = {}; -let configurationState: any; let readFromCartEntryObs: Observable; @@ -148,23 +147,21 @@ class MockLoggerService { } describe('ConfiguratorCartEffect', () => { - let addToCartMock: jasmine.Spy; - let updateCartEntryMock: jasmine.Spy; + let addToCartMock: vi.Mock; + let updateCartEntryMock: vi.Mock; - let readConfigurationForOrderEntryMock: jasmine.Spy; + let readConfigurationForOrderEntryMock: vi.Mock; let configCartEffects: fromEffects.ConfiguratorCartEffects; + let store: Store; let actions$: Observable; beforeEach(() => { - addToCartMock = jasmine.createSpy().and.returnValue(of(cartModification)); - updateCartEntryMock = jasmine - .createSpy() - .and.returnValue(of(cartModification)); - - readConfigurationForOrderEntryMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration)); + addToCartMock = vi.fn().mockReturnValue(of(cartModification)); + updateCartEntryMock = vi.fn().mockReturnValue(of(cartModification)); + readConfigurationForOrderEntryMock = vi + .fn() + .mockReturnValue(of(productConfiguration)); class MockConnector { addToCart = addToCartMock; @@ -175,7 +172,7 @@ describe('ConfiguratorCartEffect', () => { TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({}), - StoreModule.forFeature(CONFIGURATOR_FEATURE, getConfiguratorReducers), + StoreModule.forFeature(CONFIGURATOR_FEATURE, getConfiguratorReducers()), ], providers: [ fromEffects.ConfiguratorCartEffects, @@ -197,6 +194,7 @@ describe('ConfiguratorCartEffect', () => { configCartEffects = TestBed.inject( fromEffects.ConfiguratorCartEffects as Type ); + store = TestBed.inject(Store as Type>); payloadInputUpdateConfiguration = { userId: userId, @@ -204,11 +202,10 @@ describe('ConfiguratorCartEffect', () => { configuration: productConfiguration, cartEntryNumber: entryNumber.toString(), }; + }); - entitiesInConfigurationState = {}; - configurationState = { - configurations: { entities: entitiesInConfigurationState }, - }; + afterEach(() => { + vi.restoreAllMocks(); }); it('should provide configuration effects', () => { @@ -217,8 +214,14 @@ describe('ConfiguratorCartEffect', () => { describe('Effect addOwner', () => { it('should emit 2 result actions', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(productConfiguration) + store.dispatch( + new ConfiguratorActions.CreateConfigurationSuccess(productConfiguration) + ); + store.dispatch( + new ConfiguratorActions.SetInteractionState({ + entityKey: productConfiguration.owner.key, + interactionState: productConfiguration.interactionState, + }) ); const addOwnerAction = new ConfiguratorActions.AddNextOwner({ ownerKey: productConfiguration.owner.key, @@ -251,17 +254,17 @@ describe('ConfiguratorCartEffect', () => { describe('Effect removeCartBoundConfigurations', () => { it('should emit remove configuration action for configurations that belong to cart entries', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(configurationState) - ); - const configurationCartBound: Configurator.Configuration = ConfiguratorTestUtils.createConfiguration('6514', ownerCartEntry); - entitiesInConfigurationState[productConfiguration.owner.key] = - productConfiguration.owner.key; - entitiesInConfigurationState[configurationCartBound.owner.key] = - configurationCartBound.owner.key; + store.dispatch( + new ConfiguratorActions.CreateConfigurationSuccess(productConfiguration) + ); + store.dispatch( + new ConfiguratorActions.CreateConfigurationSuccess( + configurationCartBound + ) + ); const removeCartBoundConfigurationsAction = new ConfiguratorActions.RemoveCartBoundConfigurations(); @@ -281,19 +284,16 @@ describe('ConfiguratorCartEffect', () => { }); it('should emit remove configuration action for configurations that have been turned into cart configurations', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(configurationState) - ); const configurationProductBoundObsolete: Configurator.Configuration = ConfiguratorTestUtils.createConfiguration('6514', owner); configurationProductBoundObsolete.nextOwner = ownerCartEntry; - entitiesInConfigurationState[ - configurationProductBoundObsolete.owner.key - ] = { - value: configurationProductBoundObsolete, - }; + store.dispatch( + new ConfiguratorActions.CreateConfigurationSuccess( + configurationProductBoundObsolete + ) + ); const removeCartBoundConfigurationsAction = new ConfiguratorActions.RemoveCartBoundConfigurations(); @@ -313,22 +313,22 @@ describe('ConfiguratorCartEffect', () => { }); it('should not emit remove configuration action for configurations that are purely product bound or order bound', () => { - spyOnProperty(ngrxStore, 'select').and.returnValue( - () => () => of(configurationState) - ); const configurationProductBound: Configurator.Configuration = ConfiguratorTestUtils.createConfiguration('6514', owner); const configurationOrderBound: Configurator.Configuration = ConfiguratorTestUtils.createConfiguration('6513', ownerOrderEntry); - entitiesInConfigurationState[configurationProductBound.owner.key] = { - value: configurationProductBound, - }; - - entitiesInConfigurationState[configurationOrderBound.owner.key] = { - value: configurationOrderBound, - }; + store.dispatch( + new ConfiguratorActions.CreateConfigurationSuccess( + configurationProductBound + ) + ); + store.dispatch( + new ConfiguratorActions.CreateConfigurationSuccess( + configurationOrderBound + ) + ); const removeCartBoundConfigurationsAction = new ConfiguratorActions.RemoveCartBoundConfigurations(); @@ -496,7 +496,7 @@ describe('ConfiguratorCartEffect', () => { }); it('should emit a fail action if something goes wrong', () => { - readConfigurationForOrderEntryMock.and.returnValue( + readConfigurationForOrderEntryMock.mockReturnValue( throwError(() => errorResponse) ); const readFromOrderEntry: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = @@ -558,7 +558,7 @@ describe('ConfiguratorCartEffect', () => { }); it('should emit CartAddEntryFail in case add to cart call does not return entry', () => { - addToCartMock.and.returnValue(of(cartModificationWithoutEntry)); + addToCartMock.mockReturnValue(of(cartModificationWithoutEntry)); const payloadInput: Configurator.AddToCartParameters = { userId: userId, cartId: cartId, @@ -586,7 +586,7 @@ describe('ConfiguratorCartEffect', () => { }); it('should emit CartAddEntryFail in case add to cart call is not successful', () => { - addToCartMock.and.returnValue(throwError(() => errorResponse)); + addToCartMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput: Configurator.AddToCartParameters = { userId: userId, cartId: cartId, @@ -633,7 +633,7 @@ describe('ConfiguratorCartEffect', () => { }); it('should emit AddToCartFail in case update cart entry call is not successful', () => { - updateCartEntryMock.and.returnValue(throwError(() => errorResponse)); + updateCartEntryMock.mockReturnValue(throwError(() => errorResponse)); const action = new ConfiguratorActions.UpdateCartEntry( payloadInputUpdateConfiguration diff --git a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-variant.effect.spec.ts b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-variant.effect.spec.ts index 84fbe45f69f..cf74fd2c4e5 100644 --- a/feature-libs/product-configurator/rulebased/core/state/effects/configurator-variant.effect.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/state/effects/configurator-variant.effect.spec.ts @@ -25,6 +25,7 @@ import { ConfiguratorActions } from '../actions/index'; import { CONFIGURATOR_FEATURE } from '../configurator-state'; import { getConfiguratorReducers } from '../reducers/index'; import * as fromEffects from './configurator-variant.effect'; +import { vi } from 'vitest'; const productCode = 'CONF_LAPTOP'; @@ -60,14 +61,14 @@ class MockLoggerService { } describe('ConfiguratorVariantEffect', () => { - let searchVariantsMock: jasmine.Spy; + let searchVariantsMock: vi.Mock; let configEffects: fromEffects.ConfiguratorVariantEffects; let actions$: Observable; beforeEach(() => { - searchVariantsMock = jasmine.createSpy().and.returnValue(of(variants)); + searchVariantsMock = vi.fn().mockReturnValue(of(variants)); configuratorCoreConfig = { productConfigurator: { enableVariantSearch: true }, }; @@ -169,7 +170,7 @@ describe('ConfiguratorVariantEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - searchVariantsMock.and.returnValue(throwError(() => errorResponse)); + searchVariantsMock.mockReturnValue(throwError(() => errorResponse)); const action = new ConfiguratorActions.SearchVariants(productConfiguration); diff --git a/feature-libs/product-configurator/rulebased/core/state/selectors/configurator.selector.spec.ts b/feature-libs/product-configurator/rulebased/core/state/selectors/configurator.selector.spec.ts index 41176d3ad41..2e5f95709fd 100644 --- a/feature-libs/product-configurator/rulebased/core/state/selectors/configurator.selector.spec.ts +++ b/feature-libs/product-configurator/rulebased/core/state/selectors/configurator.selector.spec.ts @@ -15,6 +15,7 @@ import { import { getConfiguratorReducers } from '../reducers/index'; import { ConfiguratorTestUtils } from './../../../testing/configurator-test-utils'; import { ConfiguratorSelectors } from './index'; +import { vi } from 'vitest'; describe('Configurator selectors', () => { let store: Store; @@ -61,7 +62,7 @@ describe('Configurator selectors', () => { }; configuratorUtils.setOwnerKey(owner); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); it('should return empty content when selecting with content selector initially', () => { diff --git a/feature-libs/product-configurator/rulebased/cpq/common/converters/cpq-configurator-overview-normalizer.spec.ts b/feature-libs/product-configurator/rulebased/cpq/common/converters/cpq-configurator-overview-normalizer.spec.ts index fc94105534e..3f3449c0edb 100644 --- a/feature-libs/product-configurator/rulebased/cpq/common/converters/cpq-configurator-overview-normalizer.spec.ts +++ b/feature-libs/product-configurator/rulebased/cpq/common/converters/cpq-configurator-overview-normalizer.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { LanguageService, TranslationService } from '@spartacus/core'; import { Configurator } from '@spartacus/product-configurator/rulebased'; import { Observable, of } from 'rxjs'; @@ -147,7 +147,7 @@ class MockTranslationService { describe('CpqConfiguratorOverviewNormalizer', () => { let serviceUnderTest: CpqConfiguratorOverviewNormalizer; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ CpqConfiguratorOverviewNormalizer, @@ -167,7 +167,7 @@ describe('CpqConfiguratorOverviewNormalizer', () => { CpqConfiguratorOverviewNormalizer as Type ); attr = structuredClone(attrBase); - })); + }); it('should be created', () => { expect(serviceUnderTest).toBeDefined(); diff --git a/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.adapter.spec.ts b/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.adapter.spec.ts index f860ecba559..6a0f38d59f2 100644 --- a/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.adapter.spec.ts +++ b/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.adapter.spec.ts @@ -12,6 +12,7 @@ import { of } from 'rxjs'; import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils'; import { CpqConfiguratorOccAdapter } from './cpq-configurator-occ.adapter'; import { CpqConfiguratorOccService } from './cpq-configurator-occ.service'; +import { vi } from 'vitest'; import { provideHttpClient, withInterceptorsFromDi, @@ -83,66 +84,70 @@ const readConfigOrderEntryParams: CommonConfigurator.ReadConfigurationFromOrderE owner: owner, }; -const asSpy = (f: any) => f; +const asSpy = (f: any) => f; describe('CpqConfiguratorOccAdapter', () => { let adapterUnderTest: CpqConfiguratorOccAdapter; let mockedOccService: CpqConfiguratorOccService; beforeEach(() => { - mockedOccService = jasmine.createSpyObj('mockedOccService', [ - 'addToCart', - 'getConfigIdForCartEntry', - 'getConfigIdForOrderEntry', - 'updateCartEntry', - 'createConfiguration', - 'readConfiguration', - 'updateAttribute', - 'updateValueQuantity', - 'readConfigurationOverview', - 'readConfigurationForCartEntry', - 'readConfigurationForOrderEntry', - 'readConfigurationForQuoteEntry', - ]); - - asSpy(mockedOccService.createConfiguration).and.callFake(() => { + mockedOccService = { + addToCart: vi.fn(), + getConfigIdForCartEntry: vi.fn(), + getConfigIdForOrderEntry: vi.fn(), + updateCartEntry: vi.fn(), + createConfiguration: vi.fn(), + readConfiguration: vi.fn(), + updateAttribute: vi.fn(), + updateValueQuantity: vi.fn(), + readConfigurationOverview: vi.fn(), + readConfigurationForCartEntry: vi.fn(), + readConfigurationForOrderEntry: vi.fn(), + readConfigurationForQuoteEntry: vi.fn(), + } as any; + + asSpy(mockedOccService.createConfiguration).mockImplementation(() => { return of(productConfiguration); }); - asSpy(mockedOccService.readConfiguration).and.callFake(() => { + asSpy(mockedOccService.readConfiguration).mockImplementation(() => { return of(productConfiguration); }); - asSpy(mockedOccService.updateAttribute).and.callFake(() => { + asSpy(mockedOccService.updateAttribute).mockImplementation(() => { return of(productConfiguration); }); - asSpy(mockedOccService.updateValueQuantity).and.callFake(() => { + asSpy(mockedOccService.updateValueQuantity).mockImplementation(() => { return of(productConfiguration); }); - asSpy(mockedOccService.readConfigurationOverview).and.callFake(() => { + asSpy(mockedOccService.readConfigurationOverview).mockImplementation(() => { return of(productConfiguration); }); - asSpy(mockedOccService.addToCart).and.callFake(() => { + asSpy(mockedOccService.addToCart).mockImplementation(() => { return of(cartResponse); }); - asSpy(mockedOccService.getConfigIdForCartEntry).and.callFake(() => { + asSpy(mockedOccService.getConfigIdForCartEntry).mockImplementation(() => { return of(productConfiguration.configId); }); - asSpy(mockedOccService.getConfigIdForOrderEntry).and.callFake(() => { + asSpy(mockedOccService.getConfigIdForOrderEntry).mockImplementation(() => { return of(productConfiguration.configId); }); - asSpy(mockedOccService.updateCartEntry).and.callFake(() => { + asSpy(mockedOccService.updateCartEntry).mockImplementation(() => { return of(cartResponse); }); - asSpy(mockedOccService.getConfigIdForCartEntry).and.callFake(() => { + asSpy(mockedOccService.getConfigIdForCartEntry).mockImplementation(() => { return of(productConfiguration.configId); }); - asSpy(mockedOccService.readConfigurationForCartEntry).and.callFake(() => { - return of(productConfiguration); - }); - asSpy(mockedOccService.readConfigurationForOrderEntry).and.callFake(() => { - return of(productConfiguration); - }); + asSpy(mockedOccService.readConfigurationForCartEntry).mockImplementation( + () => { + return of(productConfiguration); + } + ); + asSpy(mockedOccService.readConfigurationForOrderEntry).mockImplementation( + () => { + return of(productConfiguration); + } + ); TestBed.configureTestingModule({ providers: [ diff --git a/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.service.spec.ts b/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.service.spec.ts index 34fe137d784..4115be23176 100644 --- a/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.service.spec.ts +++ b/feature-libs/product-configurator/rulebased/cpq/occ/cpq-configurator-occ.service.spec.ts @@ -33,6 +33,7 @@ import { CPQ_CONFIGURATOR_UPDATE_CART_ENTRY_SERIALIZER, } from './converters/cpq-configurator-occ.converters'; import { CpqConfiguratorOccService } from './cpq-configurator-occ.service'; +import { vi } from 'vitest'; import { provideHttpClient, withInterceptorsFromDi, @@ -196,8 +197,8 @@ describe('CpqConfigurationOccService', () => { CpqConfiguratorOccService as Type ); - spyOn(occEnpointsService, 'buildUrl').and.callThrough(); - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(occEnpointsService, 'buildUrl'); + vi.spyOn(converterService, 'pipeable'); }); afterEach(() => { @@ -205,7 +206,7 @@ describe('CpqConfigurationOccService', () => { }); it('should call addToCart endpoint', () => { - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'convert'); serviceUnderTest.addToCart(addToCartParams).subscribe((response) => { expect(response).toBe(cartResponse); }); @@ -287,7 +288,7 @@ describe('CpqConfigurationOccService', () => { }); it('should call upateCart endpoint', () => { - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'convert'); serviceUnderTest.updateCartEntry(updateCartParams).subscribe((response) => { expect(response).toBe(cartResponse); }); @@ -420,7 +421,7 @@ describe('CpqConfigurationOccService', () => { }); it('should call serializer, update an attribute, retrieve configuration and call normalizer', () => { - spyOn(converterService, 'convert').and.returnValue(updateAttribute); + vi.spyOn(converterService, 'convert').mockReturnValue(updateAttribute); serviceUnderTest.updateAttribute(configuration).subscribe((config) => { expect(config.errorMessages).toBe(errorMessages); }); @@ -454,7 +455,7 @@ describe('CpqConfigurationOccService', () => { }); it('should call serializer, update an attribute value quantity, retrieve configuration and call normalizer', () => { - spyOn(converterService, 'convert').and.returnValue(updateValue); + vi.spyOn(converterService, 'convert').mockReturnValue(updateValue); serviceUnderTest.updateValueQuantity(configuration).subscribe((config) => { expect(config.errorMessages).toBe(errorMessages); }); diff --git a/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.adapter.spec.ts b/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.adapter.spec.ts index fa79899bfa3..a54cd280936 100644 --- a/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.adapter.spec.ts +++ b/feature-libs/product-configurator/rulebased/occ/variant/variant-configurator-occ.adapter.spec.ts @@ -22,7 +22,7 @@ import { ConfiguratorModelUtils, ConfiguratorType, } from '@spartacus/product-configurator/common'; -import { of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { VARIANT_CONFIGURATOR_PRICE_NORMALIZER, VariantConfiguratorOccAdapter, @@ -40,6 +40,7 @@ import { VARIANT_CONFIGURATOR_SERIALIZER, } from './variant-configurator-occ.converters'; import { OccConfigurator } from './variant-configurator-occ.models'; +import { vi } from 'vitest'; import { provideHttpClient, withInterceptorsFromDi, @@ -184,8 +185,8 @@ describe('OccConfigurationVariantAdapter', () => { ); configExpertModeService.setExpModeRequested(expMode); - spyOn(converterService, 'convert').and.callThrough(); - spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converterService, 'convert'); + vi.spyOn(occEndpointsService, 'buildUrl'); productConfigurationOcc.kbKey = undefined; }); @@ -194,17 +195,16 @@ describe('OccConfigurationVariantAdapter', () => { }); describe('createConfiguration', () => { - it('should call createConfiguration endpoint', (done) => { + it('should call createConfiguration endpoint', async () => { expMode = false; configExpertModeService.setExpModeRequested(expMode); productConfigurationOcc.kbKey = undefined; - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .createConfiguration(configuration.owner) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); //this call doesn't do the actual mapping but retrieves the map function, @@ -234,14 +234,13 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should forward configuration template id', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should forward configuration template id', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .createConfiguration(configuration.owner, CONFIG_ID_TEMPLATE) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -264,8 +263,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should call createConfiguration endpoint for expert mode', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call createConfiguration endpoint for expert mode', async () => { + vi.spyOn(converterService, 'pipeable'); productConfigurationOcc.kbKey = kbKeyOcc; occConfiguratorVariantAdapter @@ -275,8 +274,6 @@ describe('OccConfigurationVariantAdapter', () => { //check if expert mode data has been transferred to model expect(resultConfiguration.kbKey?.kbLogsys).toBe(kbLogSys); - - done(); }); //this call doesn't do the actual mapping but retrieves the map function, @@ -306,15 +303,14 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should set forceReset flag if requested', (done) => { + it('should set forceReset flag if requested', async () => { forceReset = true; - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .createConfiguration(configuration.owner, undefined, true) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -337,16 +333,15 @@ describe('OccConfigurationVariantAdapter', () => { }); }); - it('should call readConfiguration endpoint', (done) => { + it('should call readConfiguration endpoint', async () => { expMode = false; configExpertModeService.setExpModeRequested(expMode); productConfigurationOcc.kbKey = undefined; - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .readConfiguration(configId, groupId, configuration.owner) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -376,16 +371,15 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should call readConfiguration endpoint with attribute key for domain values', (done) => { + it('should call readConfiguration endpoint with attribute key for domain values', async () => { expMode = false; configExpertModeService.setExpModeRequested(expMode); productConfigurationOcc.kbKey = undefined; - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .readConfiguration(configId, groupId, configuration.owner, attributeKey) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -415,8 +409,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should call readConfiguration endpoint for expert mode', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call readConfiguration endpoint for expert mode', async () => { + vi.spyOn(converterService, 'pipeable'); productConfigurationOcc.kbKey = kbKeyOcc; occConfiguratorVariantAdapter @@ -426,8 +420,6 @@ describe('OccConfigurationVariantAdapter', () => { //check if expert mode data has been transferred to model expect(resultConfiguration.kbKey?.kbLogsys).toBe(kbLogSys); - - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -457,16 +449,15 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should call updateConfiguration endpoint', (done) => { + it('should call updateConfiguration endpoint', async () => { expMode = false; configExpertModeService.setExpModeRequested(expMode); productConfigurationOcc.kbKey = undefined; - spyOn(converterService, 'pipeable').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .updateConfiguration(configuration) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -498,8 +489,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should call updateConfiguration endpoint for expert mode', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call updateConfiguration endpoint for expert mode', async () => { + vi.spyOn(converterService, 'pipeable'); productConfigurationOcc.kbKey = kbKeyOcc; occConfiguratorVariantAdapter @@ -508,8 +499,6 @@ describe('OccConfigurationVariantAdapter', () => { expect(resultConfiguration.configId).toEqual(configId); //check if expert mode data has been transferred to model expect(resultConfiguration.kbKey?.kbLogsys).toBe(kbLogSys); - - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -541,8 +530,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should call readPriceSummary endpoint', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call readPriceSummary endpoint', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .readPriceSummary(configuration) .subscribe((resultConfiguration) => { @@ -577,7 +566,6 @@ describe('OccConfigurationVariantAdapter', () => { expect(supp3.valueSupplements[0].attributeValueKey).toBe('value_3_1'); expect(supp3.valueSupplements[1].attributeValueKey).toBe('value_3_2'); expect(supp3.valueSupplements[2].attributeValueKey).toBe('value_3_3'); - done(); } }); @@ -617,15 +605,14 @@ describe('OccConfigurationVariantAdapter', () => { cartId: documentId, cartEntryNumber: documentEntryNumber, }; - it('should call readConfigurationForCartEntry endpoint', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call readConfigurationForCartEntry endpoint', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .readConfigurationForCartEntry(params) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); expect(resultConfiguration.kbKey).toBeUndefined(); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -655,8 +642,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(productConfigurationOcc); }); - it('should try to activate expert mode if requested', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should try to activate expert mode if requested', async () => { + vi.spyOn(converterService, 'pipeable'); configExpertModeService.setExpModeRequested(true); productConfigurationOcc.kbKey = kbKeyOcc; occConfiguratorVariantAdapter @@ -666,8 +653,6 @@ describe('OccConfigurationVariantAdapter', () => { //check if expert mode data has been transferred to model expect(resultConfiguration.kbKey?.kbLogsys).toBe(kbLogSys); - - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -698,8 +683,8 @@ describe('OccConfigurationVariantAdapter', () => { }); }); - it('should call readVariantConfigurationOverviewForOrderEntry endpoint', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call readVariantConfigurationOverviewForOrderEntry endpoint', async () => { + vi.spyOn(converterService, 'pipeable'); const params: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = { owner: configuration.owner, @@ -711,7 +696,6 @@ describe('OccConfigurationVariantAdapter', () => { .readConfigurationForOrderEntry(params) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -740,8 +724,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(overviewOcc); }); - it('should call readVariantConfigurationOverviewForQuoteEntry endpoint in case ownwer is quote', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call readVariantConfigurationOverviewForQuoteEntry endpoint in case ownwer is quote', async () => { + vi.spyOn(converterService, 'pipeable'); const params: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = { owner: { @@ -756,7 +740,6 @@ describe('OccConfigurationVariantAdapter', () => { .readConfigurationForOrderEntry(params) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -785,8 +768,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(overviewOcc); }); - it('should call readVariantConfigurationOverviewForSavedCartEntry endpoint in case ownwer is savedCart', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call readVariantConfigurationOverviewForSavedCartEntry endpoint in case ownwer is savedCart', async () => { + vi.spyOn(converterService, 'pipeable'); const params: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = { owner: { @@ -801,7 +784,6 @@ describe('OccConfigurationVariantAdapter', () => { .readConfigurationForOrderEntry(params) .subscribe((resultConfiguration) => { expect(resultConfiguration.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -830,8 +812,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(overviewOcc); }); - it('should call updateVariantConfigurationForCartEntry endpoint', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call updateVariantConfigurationForCartEntry endpoint', async () => { + vi.spyOn(converterService, 'pipeable'); const params: Configurator.UpdateConfigurationForCartEntryParameters = { configuration: configuration, userId: userId, @@ -844,7 +826,6 @@ describe('OccConfigurationVariantAdapter', () => { expect(cartModificationResult.quantity).toEqual( cartModification.quantity ); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -873,8 +854,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(cartModification); }); - it('should call addToCart endpoint', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call addToCart endpoint', async () => { + vi.spyOn(converterService, 'pipeable'); const params: Configurator.AddToCartParameters = { productCode: 'Product', quantity: 1, @@ -889,7 +870,6 @@ describe('OccConfigurationVariantAdapter', () => { expect(cartModificationResult.quantity).toEqual( cartModification.quantity ); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -906,7 +886,7 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(cartModification); }); - it('should set owner on readVariantConfigurationForCartEntry according to parameters', (done) => { + it('should set owner on readVariantConfigurationForCartEntry according to parameters', async () => { const params: CommonConfigurator.ReadConfigurationFromCartEntryParameters = { owner: productConfigurationForCartEntry.owner, @@ -914,7 +894,7 @@ describe('OccConfigurationVariantAdapter', () => { cartId: documentId, cartEntryNumber: documentEntryNumber, }; - spyOn(converterService, 'pipeable').and.returnValue(() => + vi.spyOn(converterService, 'pipeable').mockReturnValue(() => of(configuration) ); occConfiguratorVariantAdapter @@ -924,17 +904,15 @@ describe('OccConfigurationVariantAdapter', () => { expect(owner).toBeDefined(); expect(owner.type).toBe(CommonConfigurator.OwnerType.CART_ENTRY); expect(owner.id).toBe(cartEntryNo); - done(); }); }); - it('should call getVariantConfigurationOverview endpoint', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call getVariantConfigurationOverview endpoint', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .getConfigurationOverview(configuration.configId) .subscribe((productConfigurationResult) => { expect(productConfigurationResult.configId).toBe(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -963,12 +941,11 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(overviewOcc); }); - it('should call searchConfiguratorVariants endpoint', (done) => { + it('should call searchConfiguratorVariants endpoint', async () => { occConfiguratorVariantAdapter .searchVariants(configuration.configId) .subscribe((productConfigurationResult) => { expect(productConfigurationResult.length).toBe(1); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -1007,14 +984,13 @@ describe('OccConfigurationVariantAdapter', () => { attributeFilters: [Configurator.OverviewFilter.PRICE_RELEVANT], possibleGroups: [{ id: '1' }, { id: '2' }], }; - it('should call overview endpoint and build url', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should call overview endpoint and build url', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .updateConfigurationOverview(overviewInput) .subscribe((resultOv) => { expect(resultOv.configId).toEqual(configId); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -1041,8 +1017,8 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(overviewOcc); }); - it('should return filter attributes like provided as input', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should return filter attributes like provided as input', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .updateConfigurationOverview(overviewInput) @@ -1051,7 +1027,6 @@ describe('OccConfigurationVariantAdapter', () => { overviewInput.attributeFilters ); expect(resultOv.groupFilters).toEqual(overviewInput.groupFilters); - done(); }); const mockReq = httpMock.expectOne((req) => { @@ -1063,14 +1038,13 @@ describe('OccConfigurationVariantAdapter', () => { mockReq.flush(overviewOcc); }); - it('should return possible groups like provided as input', (done) => { - spyOn(converterService, 'pipeable').and.callThrough(); + it('should return possible groups like provided as input', async () => { + vi.spyOn(converterService, 'pipeable'); occConfiguratorVariantAdapter .updateConfigurationOverview(overviewInput) .subscribe((resultOv) => { expect(resultOv.possibleGroups).toEqual(overviewInput.possibleGroups); - done(); }); const mockReq = httpMock.expectOne((req) => { diff --git a/feature-libs/product-configurator/rulebased/root/cpq/cpq-configurator-page-layout-handler.spec.ts b/feature-libs/product-configurator/rulebased/root/cpq/cpq-configurator-page-layout-handler.spec.ts index 31f3b186234..d8dec7939fa 100644 --- a/feature-libs/product-configurator/rulebased/root/cpq/cpq-configurator-page-layout-handler.spec.ts +++ b/feature-libs/product-configurator/rulebased/root/cpq/cpq-configurator-page-layout-handler.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ConfiguratorModelUtils, ConfiguratorRouter, @@ -92,7 +92,7 @@ const sectionContent = 'content'; describe('CpqConfiguratorPageLayoutHandler', () => { let classUnderTest: CpqConfiguratorPageLayoutHandler; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ { @@ -109,7 +109,7 @@ describe('CpqConfiguratorPageLayoutHandler', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( CpqConfiguratorPageLayoutHandler as Type diff --git a/feature-libs/product-configurator/rulebased/root/http-interceptors/configurator-bad-request.handler.spec.ts b/feature-libs/product-configurator/rulebased/root/http-interceptors/configurator-bad-request.handler.spec.ts index 08f9d81c1d9..fb0926e16f2 100644 --- a/feature-libs/product-configurator/rulebased/root/http-interceptors/configurator-bad-request.handler.spec.ts +++ b/feature-libs/product-configurator/rulebased/root/http-interceptors/configurator-bad-request.handler.spec.ts @@ -8,6 +8,7 @@ import { Priority, } from '@spartacus/core'; import { ConfiguratorBadRequestHandler } from '@spartacus/product-configurator/rulebased/root'; +import { vi } from 'vitest'; const mockRequest = {} as HttpRequest; @@ -98,14 +99,14 @@ describe('ConfiguratorBadRequestHandler', () => { describe('handleError', () => { it('should be able to deal with an empty error response', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockEmptyResponse); expect(globalMessageService.add).toHaveBeenCalledTimes(0); }); it('should raise no message for IllegalStateError that are not related to make-to-stock', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError( mockRequest, @@ -116,7 +117,7 @@ describe('ConfiguratorBadRequestHandler', () => { }); it('should raise a message for IllegalStateError that are related to make-to-stock', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError( mockRequest, diff --git a/feature-libs/product-configurator/rulebased/root/variant/variant-configurator-page-layout-handler.spec.ts b/feature-libs/product-configurator/rulebased/root/variant/variant-configurator-page-layout-handler.spec.ts index cc7b09ddf15..08fa5d5b39c 100644 --- a/feature-libs/product-configurator/rulebased/root/variant/variant-configurator-page-layout-handler.spec.ts +++ b/feature-libs/product-configurator/rulebased/root/variant/variant-configurator-page-layout-handler.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ConfiguratorModelUtils, ConfiguratorRouter, @@ -66,7 +66,7 @@ const sectionContent = 'content'; describe('VariantConfiguratorPageLayoutHandler', () => { let classUnderTest: VariantConfiguratorPageLayoutHandler; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ { @@ -83,7 +83,7 @@ describe('VariantConfiguratorPageLayoutHandler', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { classUnderTest = TestBed.inject( VariantConfiguratorPageLayoutHandler as Type diff --git a/feature-libs/product-configurator/test.ts b/feature-libs/product-configurator/test.ts deleted file mode 100644 index 381a72c5ff2..00000000000 --- a/feature-libs/product-configurator/test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -// Patching Object.defineProperty unlocks frozen JS symbols and makes possible to mock them. -// Should be used with caution, and only if there is no other way to mock stuff (eg. by DI) -// Has to be imported just after zone.js imports. -import 'testing/patch-object-define-property'; - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/product-configurator/textfield/components/add-to-cart-button/configurator-textfield-add-to-cart-button.component.spec.ts b/feature-libs/product-configurator/textfield/components/add-to-cart-button/configurator-textfield-add-to-cart-button.component.spec.ts index 2af8201514d..5413c014bcf 100644 --- a/feature-libs/product-configurator/textfield/components/add-to-cart-button/configurator-textfield-add-to-cart-button.component.spec.ts +++ b/feature-libs/product-configurator/textfield/components/add-to-cart-button/configurator-textfield-add-to-cart-button.component.spec.ts @@ -4,7 +4,7 @@ import { PipeTransform, Type, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterModule } from '@angular/router'; import { @@ -24,6 +24,7 @@ import { Observable, of } from 'rxjs'; import { ConfiguratorTextfieldService } from '../../core/facade/configurator-textfield.service'; import { ConfiguratorTextfield } from '../../core/model/configurator-textfield.model'; import { ConfiguratorTextfieldAddToCartButtonComponent } from './configurator-textfield-add-to-cart-button.component'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; const URL_CONFIGURATION = 'host:port/electronics-spa/en/USD/configureTEXTFIELD'; @@ -84,7 +85,7 @@ describe('ConfigTextfieldAddToCartButtonComponent', () => { expect(seenText).toBe(buttonText); } - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ConfiguratorTextfieldAddToCartButtonComponent, @@ -111,7 +112,7 @@ describe('ConfigTextfieldAddToCartButtonComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -145,7 +146,7 @@ describe('ConfigTextfieldAddToCartButtonComponent', () => { }); it('should navigate to cart and call addToCart on core service when onAddToCart was triggered ', () => { - spyOn(textfieldService, 'addToCart').and.callThrough(); + vi.spyOn(textfieldService, 'addToCart'); classUnderTest.onAddToCart(); @@ -158,7 +159,7 @@ describe('ConfigTextfieldAddToCartButtonComponent', () => { it('should navigate to cart when onAddToCart was triggered and owner points to cart entry ', () => { OWNER.type = CommonConfigurator.OwnerType.CART_ENTRY; - spyOn(textfieldService, 'updateCartEntry').and.callThrough(); + vi.spyOn(textfieldService, 'updateCartEntry'); classUnderTest.onAddToCart(); expect(textfieldService.updateCartEntry).toHaveBeenCalledWith( diff --git a/feature-libs/product-configurator/textfield/components/form/configurator-textfield-form.component.spec.ts b/feature-libs/product-configurator/textfield/components/form/configurator-textfield-form.component.spec.ts index 121707173fd..37432ce7f93 100644 --- a/feature-libs/product-configurator/textfield/components/form/configurator-textfield-form.component.spec.ts +++ b/feature-libs/product-configurator/textfield/components/form/configurator-textfield-form.component.spec.ts @@ -1,5 +1,5 @@ import { Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; import { @@ -19,6 +19,7 @@ import { ConfiguratorTextfield } from '../../core/model/configurator-textfield.m import { ConfiguratorTextfieldAddToCartButtonComponent } from '../add-to-cart-button/configurator-textfield-add-to-cart-button.component'; import { ConfiguratorTextfieldInputFieldComponent } from '../input-field/configurator-textfield-input-field.component'; import { ConfiguratorTextfieldFormComponent } from './configurator-textfield-form.component'; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; const CART_ENTRY_KEY = '3'; @@ -77,7 +78,7 @@ describe('TextfieldFormComponent', () => { let fixture: ComponentFixture; let textfieldService: ConfiguratorTextfieldService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -99,7 +100,7 @@ describe('TextfieldFormComponent', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorTextfieldFormComponent); component = fixture.componentInstance; @@ -160,7 +161,7 @@ describe('TextfieldFormComponent', () => { }); it('should call update configuration on facade in case it was triggered on component', () => { - spyOn(textfieldService, 'updateConfiguration').and.callThrough(); + vi.spyOn(textfieldService, 'updateConfiguration'); component.updateConfiguration(productConfig.configurationInfos[0]); expect(textfieldService.updateConfiguration).toHaveBeenCalledTimes(1); }); diff --git a/feature-libs/product-configurator/textfield/components/input-field-readonly/configurator-textfield-input-field-readonly.component.spec.ts b/feature-libs/product-configurator/textfield/components/input-field-readonly/configurator-textfield-input-field-readonly.component.spec.ts index ea4b9119422..36271dd357e 100644 --- a/feature-libs/product-configurator/textfield/components/input-field-readonly/configurator-textfield-input-field-readonly.component.spec.ts +++ b/feature-libs/product-configurator/textfield/components/input-field-readonly/configurator-textfield-input-field-readonly.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule } from '@spartacus/core'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; @@ -10,14 +10,14 @@ describe('TextfieldInputFieldReadonlyComponent', () => { let htmlElem: HTMLElement; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, ConfiguratorTextfieldInputFieldReadonlyComponent, ], }); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -29,14 +29,15 @@ describe('TextfieldInputFieldReadonlyComponent', () => { configurationLabel: 'attributeName', configurationValue: ATTRIBUTE_VALUE, }; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it('should render a visually hidden span', () => { + fixture.detectChanges(); const idLabel = component.getIdLabel(component.attribute); const elementsSpan = htmlElem.querySelectorAll('#' + idLabel); expect(elementsSpan.length).toBe(1); @@ -46,6 +47,7 @@ describe('TextfieldInputFieldReadonlyComponent', () => { }); it('should render a hidden label', () => { + fixture.detectChanges(); const elementsLabel = htmlElem.querySelectorAll('label'); expect(elementsLabel.length).toBe(1); const elementLabel = elementsLabel[0]; @@ -53,6 +55,7 @@ describe('TextfieldInputFieldReadonlyComponent', () => { }); it('should render a value', () => { + fixture.detectChanges(); const elementsDiv = htmlElem.querySelectorAll('div'); expect(elementsDiv.length).toBe(1); const elementDiv = elementsDiv[0]; @@ -61,6 +64,7 @@ describe('TextfieldInputFieldReadonlyComponent', () => { describe('Accessibility', () => { it("should contain span element with class name 'cx-visually-hidden' and its corresponding introduction text", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -74,6 +78,7 @@ describe('TextfieldInputFieldReadonlyComponent', () => { }); it("should contain label element with 'aria-hidden' attribute and its 'true' value", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -86,6 +91,7 @@ describe('TextfieldInputFieldReadonlyComponent', () => { }); it("should contain label element with 'aria-describedby' attribute and its reference to corresponding value", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -98,6 +104,7 @@ describe('TextfieldInputFieldReadonlyComponent', () => { }); it("should contain div element with 'aria-hidden' attribute and its 'true' value", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/textfield/components/input-field/configurator-textfield-input-field.component.spec.ts b/feature-libs/product-configurator/textfield/components/input-field/configurator-textfield-input-field.component.spec.ts index 5733fccdebf..1ad5b9d7337 100644 --- a/feature-libs/product-configurator/textfield/components/input-field/configurator-textfield-input-field.component.spec.ts +++ b/feature-libs/product-configurator/textfield/components/input-field/configurator-textfield-input-field.component.spec.ts @@ -1,15 +1,16 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { I18nTestingModule } from '@spartacus/core'; import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service'; import { ConfiguratorTextfieldInputFieldComponent } from './configurator-textfield-input-field.component'; +import { vi } from 'vitest'; describe('TextfieldInputFieldComponent', () => { let component: ConfiguratorTextfieldInputFieldComponent; let fixture: ComponentFixture; let htmlElem: HTMLElement; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -17,7 +18,7 @@ describe('TextfieldInputFieldComponent', () => { ConfiguratorTextfieldInputFieldComponent, ], }); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ConfiguratorTextfieldInputFieldComponent); @@ -27,19 +28,21 @@ describe('TextfieldInputFieldComponent', () => { configurationLabel: 'attributeName', configurationValue: 'input123', }; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it('should set value on init', () => { + fixture.detectChanges(); expect(component.attributeInputForm.value).toEqual('input123'); }); it('should emit a change event on change ', () => { - spyOn(component.inputChange, 'emit').and.callThrough(); + fixture.detectChanges(); + vi.spyOn(component.inputChange, 'emit'); component.onInputChange(); expect(component.inputChange.emit).toHaveBeenCalledWith( component.attribute @@ -47,6 +50,7 @@ describe('TextfieldInputFieldComponent', () => { }); it('should generate id with prefixt', () => { + fixture.detectChanges(); expect(component.getId(component.attribute)).toEqual( 'cx-configurator-textfieldattributeName' ); @@ -54,6 +58,7 @@ describe('TextfieldInputFieldComponent', () => { describe('Accessibility', () => { it("should contain label element with class name 'cx-configurator-textfield-label' and 'aria-label' attribute", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, @@ -66,6 +71,7 @@ describe('TextfieldInputFieldComponent', () => { }); it("should contain input element with class name 'form-control' and 'aria-label' attribute", () => { + fixture.detectChanges(); CommonConfiguratorTestUtilsService.expectElementContainsA11y( expect, htmlElem, diff --git a/feature-libs/product-configurator/textfield/core/connectors/configurator-textfield.connector.spec.ts b/feature-libs/product-configurator/textfield/core/connectors/configurator-textfield.connector.spec.ts index 6334fb7fe1c..31c8c745b68 100644 --- a/feature-libs/product-configurator/textfield/core/connectors/configurator-textfield.connector.spec.ts +++ b/feature-libs/product-configurator/textfield/core/connectors/configurator-textfield.connector.spec.ts @@ -9,8 +9,7 @@ import { of } from 'rxjs'; import { ConfiguratorTextfield } from '../model/configurator-textfield.model'; import { ConfiguratorTextfieldAdapter } from './configurator-textfield.adapter'; import { ConfiguratorTextfieldConnector } from './configurator-textfield.connector'; - -import createSpy = jasmine.createSpy; +import { vi } from 'vitest'; const USER_ID = 'theUser'; const CART_ID = '98876'; @@ -22,22 +21,22 @@ const configuration: ConfiguratorTextfield.Configuration = { const cartModification: CartModification = {}; class MockConfiguratorTextfieldAdapter implements ConfiguratorTextfieldAdapter { - readConfiguration = createSpy().and.callFake(() => of(configuration)); + readConfiguration = vi.fn().mockImplementation(() => of(configuration)); - createConfiguration = createSpy().and.callFake(() => of(configuration)); + createConfiguration = vi.fn().mockImplementation(() => of(configuration)); - addToCart = createSpy().and.callFake(() => of(cartModification)); + addToCart = vi.fn().mockImplementation(() => of(cartModification)); - updateConfigurationForCartEntry = createSpy().and.callFake(() => - of(cartModification) - ); + updateConfigurationForCartEntry = vi + .fn() + .mockImplementation(() => of(cartModification)); - readConfigurationForCartEntry = createSpy().and.callFake(() => - of(configuration) - ); - readConfigurationForOrderEntry = createSpy().and.callFake(() => - of(configuration) - ); + readConfigurationForCartEntry = vi + .fn() + .mockImplementation(() => of(configuration)); + readConfigurationForOrderEntry = vi + .fn() + .mockImplementation(() => of(configuration)); } describe('ConfiguratorTextfieldConnector', () => { diff --git a/feature-libs/product-configurator/textfield/core/facade/configurator-textfield.service.spec.ts b/feature-libs/product-configurator/textfield/core/facade/configurator-textfield.service.spec.ts index 8c62cfd1215..bfabb09aad4 100644 --- a/feature-libs/product-configurator/textfield/core/facade/configurator-textfield.service.spec.ts +++ b/feature-libs/product-configurator/textfield/core/facade/configurator-textfield.service.spec.ts @@ -1,6 +1,5 @@ import { Type } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; -import * as ngrxStore from '@ngrx/store'; +import { TestBed } from '@angular/core/testing'; import { Store, StoreModule } from '@ngrx/store'; import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; import { @@ -20,7 +19,7 @@ import { StateWithConfigurationTextfield, } from '../state/configuration-textfield-state'; import { ConfiguratorTextfieldService } from './configurator-textfield.service'; -import createSpy = jasmine.createSpy; +import { vi } from 'vitest'; const PRODUCT_CODE = 'CONF_LAPTOP'; @@ -129,17 +128,7 @@ class MockUserIdService { describe('ConfiguratorTextfieldService', () => { let serviceUnderTest: ConfiguratorTextfieldService; let store: Store; - const mockConfigLoaderStateReturned = createSpy('select').and.returnValue( - () => of(loaderState) - ); - const mockConfigLoaderStateNothingPresent = createSpy( - 'select' - ).and.returnValue(() => of(loaderStateNothingPresent)); - const mockConfigReturned = createSpy('select').and.returnValue(() => - of(productConfiguration) - ); - - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreModule.forRoot({})], providers: [ @@ -154,7 +143,7 @@ describe('ConfiguratorTextfieldService', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { serviceUnderTest = TestBed.inject( ConfiguratorTextfieldService as Type @@ -163,7 +152,7 @@ describe('ConfiguratorTextfieldService', () => { Store as Type> ); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); it('should create service', () => { @@ -171,9 +160,10 @@ describe('ConfiguratorTextfieldService', () => { }); describe('createConfiguration', () => { it('should return a configuration if one is present', () => { - spyOnProperty(ngrxStore, 'select').and.returnValues( - mockConfigLoaderStateReturned - ); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, tapOp, mapOp, filterOp, mapOp2] = _ops; + return of(loaderState).pipe(tapOp, mapOp, filterOp, mapOp2); + }); const configurationFromStore = serviceUnderTest.createConfiguration(owner); @@ -187,9 +177,15 @@ describe('ConfiguratorTextfieldService', () => { }); it('should create a configuration if nothing is present in store yet', () => { - spyOnProperty(ngrxStore, 'select').and.returnValues( - mockConfigLoaderStateNothingPresent - ); + vi.spyOn(store, 'pipe').mockImplementationOnce((..._ops: any[]) => { + const [, tapOp, mapOp, filterOp, mapOp2] = _ops; + return of(loaderStateNothingPresent).pipe( + tapOp, + mapOp, + filterOp, + mapOp2 + ); + }); const configurationFromStore = serviceUnderTest.createConfiguration(owner); @@ -220,7 +216,7 @@ describe('ConfiguratorTextfieldService', () => { }); it('should dispatch the correct action when readFromCartEntry is called', () => { - spyOnProperty(ngrxStore, 'select').and.returnValues(mockConfigReturned); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); const configurationFromStore = serviceUnderTest.readConfigurationForCartEntry(ownerCartRelated); @@ -240,7 +236,7 @@ describe('ConfiguratorTextfieldService', () => { }); it('should dispatch the correct action when readConfigurationForOrderEntry is called', () => { - spyOnProperty(ngrxStore, 'select').and.returnValues(mockConfigReturned); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); const configurationFromStore = serviceUnderTest.readConfigurationForOrderEntry(ownerOrderRelated); @@ -260,23 +256,18 @@ describe('ConfiguratorTextfieldService', () => { }); it('should access the store when calling createConfiguration', () => { - spyOnProperty(ngrxStore, 'select').and.returnValues( - mockConfigLoaderStateReturned - ); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(loaderState)); serviceUnderTest .createConfiguration(owner) .subscribe((configurationFromStore) => - expect(configurationFromStore).toBe(productConfiguration) + expect(configurationFromStore).toEqual(loaderState) ) .unsubscribe(); }); it('should update a configuration, accessing the store', () => { - spyOnProperty(ngrxStore, 'select').and.returnValues(mockConfigReturned); - spyOn( - serviceUnderTest, - 'createNewConfigurationWithChange' - ).and.callThrough(); + vi.spyOn(store, 'pipe').mockReturnValueOnce(of(productConfiguration)); + vi.spyOn(serviceUnderTest, 'createNewConfigurationWithChange'); serviceUnderTest.updateConfiguration(changedAttribute); diff --git a/feature-libs/product-configurator/textfield/core/state/effects/configurator-textfield.effect.spec.ts b/feature-libs/product-configurator/textfield/core/state/effects/configurator-textfield.effect.spec.ts index eac24ffca63..1da0191c430 100644 --- a/feature-libs/product-configurator/textfield/core/state/effects/configurator-textfield.effect.spec.ts +++ b/feature-libs/product-configurator/textfield/core/state/effects/configurator-textfield.effect.spec.ts @@ -23,6 +23,7 @@ import { ConfiguratorTextfieldActions } from '../actions/index'; import { CONFIGURATION_TEXTFIELD_FEATURE } from '../configuration-textfield-state'; import * as reducers from '../reducers/index'; import * as fromEffects from './configurator-textfield.effect'; +import { vi } from 'vitest'; const productCode = 'CONF_LAPTOP'; const cartId = 'CART-1234'; @@ -56,30 +57,23 @@ class MockLoggerService { } describe('ConfiguratorTextfieldEffect', () => { - let createMock: jasmine.Spy; - let readFromCartEntryMock: jasmine.Spy; - let readFromOrderEntryMock: jasmine.Spy; + let createMock: vi.Mock; + let readFromCartEntryMock: vi.Mock; + let readFromOrderEntryMock: vi.Mock; - let addToCartMock: jasmine.Spy; - let updateCartEntryMock: jasmine.Spy; + let addToCartMock: vi.Mock; + let updateCartEntryMock: vi.Mock; let configEffects: fromEffects.ConfiguratorTextfieldEffects; let actions$: Observable; beforeEach(() => { - createMock = jasmine.createSpy().and.returnValue(of(productConfiguration)); - readFromCartEntryMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration)); - readFromOrderEntryMock = jasmine - .createSpy() - .and.returnValue(of(productConfiguration)); - - addToCartMock = jasmine.createSpy().and.returnValue(of(cartModification)); - updateCartEntryMock = jasmine - .createSpy() - .and.returnValue(of(cartModification)); + createMock = vi.fn().mockReturnValue(of(productConfiguration)); + readFromCartEntryMock = vi.fn().mockReturnValue(of(productConfiguration)); + readFromOrderEntryMock = vi.fn().mockReturnValue(of(productConfiguration)); + addToCartMock = vi.fn().mockReturnValue(of(cartModification)); + updateCartEntryMock = vi.fn().mockReturnValue(of(cartModification)); class MockConnector { createConfiguration = createMock; addToCart = addToCartMock; @@ -139,7 +133,7 @@ describe('ConfiguratorTextfieldEffect', () => { }); it('should emit a fail action in case something goes wrong', () => { - createMock.and.returnValue(throwError(() => errorResponse)); + createMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput = { productCode: productCode, owner: ConfiguratorModelUtils.createInitialOwner(), @@ -180,7 +174,7 @@ describe('ConfiguratorTextfieldEffect', () => { }); it('should emit a fail action in case read from cart leads to an error', () => { - readFromCartEntryMock.and.returnValue(throwError(() => errorResponse)); + readFromCartEntryMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput: CommonConfigurator.ReadConfigurationFromCartEntryParameters = { owner: ConfiguratorModelUtils.createInitialOwner(), @@ -223,7 +217,7 @@ describe('ConfiguratorTextfieldEffect', () => { }); it('should emit a fail action in case read from order entry leads to an error', () => { - readFromOrderEntryMock.and.returnValue(throwError(() => errorResponse)); + readFromOrderEntryMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput: CommonConfigurator.ReadConfigurationFromOrderEntryParameters = { owner: ConfiguratorModelUtils.createInitialOwner(), @@ -282,7 +276,7 @@ describe('ConfiguratorTextfieldEffect', () => { }); it('should emit AddToCartFail in case add to cart call is not successful', () => { - addToCartMock.and.returnValue(throwError(() => errorResponse)); + addToCartMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput = { userId: userId, cartId: cartId, @@ -333,7 +327,7 @@ describe('ConfiguratorTextfieldEffect', () => { }); it('should emit CartUpdateEntryFail in case update cart entry is not successful', () => { - updateCartEntryMock.and.returnValue(throwError(() => errorResponse)); + updateCartEntryMock.mockReturnValue(throwError(() => errorResponse)); const payloadInput: ConfiguratorTextfield.UpdateCartEntryParameters = { userId: userId, cartId: cartId, diff --git a/feature-libs/product-configurator/textfield/occ/occ-configurator-textfield.adapter.spec.ts b/feature-libs/product-configurator/textfield/occ/occ-configurator-textfield.adapter.spec.ts index c8efb87a57a..acee11c7439 100644 --- a/feature-libs/product-configurator/textfield/occ/occ-configurator-textfield.adapter.spec.ts +++ b/feature-libs/product-configurator/textfield/occ/occ-configurator-textfield.adapter.spec.ts @@ -18,6 +18,7 @@ import { import { OccConfiguratorTextfieldAdapter } from '.'; import { CONFIGURATION_TEXTFIELD_NORMALIZER } from '../core/connectors/converters'; import { ConfiguratorTextfield } from '../core/model/configurator-textfield.model'; +import { vi } from 'vitest'; import { provideHttpClient, withInterceptorsFromDi, @@ -117,9 +118,9 @@ describe('OccConfigurationTextfieldAdapter', () => { OccConfiguratorTextfieldAdapter as Type ); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'convert').and.callThrough(); - spyOn(occEnpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'convert'); + vi.spyOn(occEnpointsService, 'buildUrl'); }); afterEach(() => { diff --git a/feature-libs/product-configurator/tsconfig.spec.json b/feature-libs/product-configurator/tsconfig.spec.json index 9d76c01da9b..d52c68cbde6 100644 --- a/feature-libs/product-configurator/tsconfig.spec.json +++ b/feature-libs/product-configurator/tsconfig.spec.json @@ -2,11 +2,18 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "strict": false, - "types": ["jasmine", "node"], "module": "preserve", - "moduleResolution": "bundler" + "strict": false, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/product-configurator/vitest.config.ts b/feature-libs/product-configurator/vitest.config.ts new file mode 100644 index 00000000000..849ea13cb45 --- /dev/null +++ b/feature-libs/product-configurator/vitest.config.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + 'core-libs/storefront/shared/test/mock-feature-level-directive': `${root}/core-libs/storefront/shared/test/mock-feature-level-directive.ts`, + 'core-libs/core/src/features-config/feature-toggles/testing': `${root}/core-libs/core/src/features-config/feature-toggles/testing/index.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module.ts`, + }, + }, + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/product-configurator`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-product-configurator.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/product-multi-dimensional/karma.conf.js b/feature-libs/product-multi-dimensional/karma.conf.js deleted file mode 100644 index bf4f071c41a..00000000000 --- a/feature-libs/product-multi-dimensional/karma.conf.js +++ /dev/null @@ -1,48 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join( - __dirname, - '../../coverage/product-multi-dimensional' - ), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 75, - functions: 85, - }, - }, - }, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/product-multi-dimensional/list/root/components/product-item-details/product-multi-dimensional-list-item-details.component.spec.ts b/feature-libs/product-multi-dimensional/list/root/components/product-item-details/product-multi-dimensional-list-item-details.component.spec.ts index 07b87013e36..8a17fb518f4 100644 --- a/feature-libs/product-multi-dimensional/list/root/components/product-item-details/product-multi-dimensional-list-item-details.component.spec.ts +++ b/feature-libs/product-multi-dimensional/list/root/components/product-item-details/product-multi-dimensional-list-item-details.component.spec.ts @@ -3,16 +3,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { I18nTestingModule, Product } from '@spartacus/core'; import { ProductListItemContext } from '@spartacus/storefront'; -import { of } from 'rxjs'; +import { Subject, of } from 'rxjs'; import { ProductMultiDimensionalListItemDetailsComponent } from './product-multi-dimensional-list-item-details.component'; describe('ProductMultiDimensionalListItemDetailsComponent', () => { let component: ProductMultiDimensionalListItemDetailsComponent; let fixture: ComponentFixture; + let productSubject: Subject; beforeEach(async () => { + productSubject = new Subject(); const mockContext = { - product$: of(), + product$: productSubject.asObservable(), }; await TestBed.configureTestingModule({ @@ -31,10 +33,11 @@ describe('ProductMultiDimensionalListItemDetailsComponent', () => { ProductMultiDimensionalListItemDetailsComponent ); component = fixture.componentInstance; - fixture.detectChanges(); + // No detectChanges() here — each test controls its own initial state }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -81,7 +84,8 @@ describe('ProductMultiDimensionalListItemDetailsComponent', () => { maxPrice: { formattedValue: '$200' }, }, }; - (component as any).product$ = of(product); + fixture.detectChanges(); + productSubject.next(product); fixture.detectChanges(); const priceElement = fixture.debugElement.query( @@ -95,7 +99,8 @@ describe('ProductMultiDimensionalListItemDetailsComponent', () => { multidimensional: false, price: { formattedValue: '$150' }, }; - (component as any).product$ = of(product); + fixture.detectChanges(); + productSubject.next(product); fixture.detectChanges(); const priceElement = fixture.debugElement.query( @@ -108,7 +113,8 @@ describe('ProductMultiDimensionalListItemDetailsComponent', () => { const product: Product = { multidimensional: false, }; - (component as any).product$ = of(product); + fixture.detectChanges(); + productSubject.next(product); fixture.detectChanges(); const priceElement = fixture.debugElement.query( @@ -126,7 +132,8 @@ describe('ProductMultiDimensionalListItemDetailsComponent', () => { maxPrice: { formattedValue: '' }, }, }; - (component as any).product$ = of(product); + fixture.detectChanges(); + productSubject.next(product); fixture.detectChanges(); const priceElement = fixture.debugElement.query( diff --git a/feature-libs/product-multi-dimensional/project.json b/feature-libs/product-multi-dimensional/project.json index 9ac10ea337d..85004fe5d96 100644 --- a/feature-libs/product-multi-dimensional/project.json +++ b/feature-libs/product-multi-dimensional/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/product-multi-dimensional/test.ts", - "tsConfig": "feature-libs/product-multi-dimensional/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/product-multi-dimensional/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "lint": { diff --git a/feature-libs/product-multi-dimensional/selector/components/guards/product-multi-dimensional-selector.guard.spec.ts b/feature-libs/product-multi-dimensional/selector/components/guards/product-multi-dimensional-selector.guard.spec.ts index 1aa82b438ba..f397d55a8c6 100644 --- a/feature-libs/product-multi-dimensional/selector/components/guards/product-multi-dimensional-selector.guard.spec.ts +++ b/feature-libs/product-multi-dimensional/selector/components/guards/product-multi-dimensional-selector.guard.spec.ts @@ -3,19 +3,18 @@ import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; import { of } from 'rxjs'; import { Product, ProductService, SemanticPathService } from '@spartacus/core'; import { ProductMultiDimensionalSelectorGuard } from './product-multi-dimensional-selector.guard'; +import { vi } from 'vitest'; describe('ProductMultiDimensionalSelectorGuard', () => { let guard: ProductMultiDimensionalSelectorGuard; - let productService: jasmine.SpyObj; - let semanticPathService: jasmine.SpyObj; - let router: jasmine.SpyObj; + let productService: any; + let semanticPathService: any; + let router: any; beforeEach(() => { - const productServiceSpy = jasmine.createSpyObj('ProductService', ['get']); - const semanticPathServiceSpy = jasmine.createSpyObj('SemanticPathService', [ - 'transform', - ]); - const routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']); + const productServiceSpy = { get: vi.fn() }; + const semanticPathServiceSpy = { transform: vi.fn() }; + const routerSpy = { createUrlTree: vi.fn() }; TestBed.configureTestingModule({ providers: [ @@ -27,13 +26,9 @@ describe('ProductMultiDimensionalSelectorGuard', () => { }); guard = TestBed.inject(ProductMultiDimensionalSelectorGuard); - productService = TestBed.inject( - ProductService - ) as jasmine.SpyObj; - semanticPathService = TestBed.inject( - SemanticPathService - ) as jasmine.SpyObj; - router = TestBed.inject(Router) as jasmine.SpyObj; + productService = TestBed.inject(ProductService) as any; + semanticPathService = TestBed.inject(SemanticPathService) as any; + router = TestBed.inject(Router) as any; }); describe('canActivate', () => { @@ -61,7 +56,7 @@ describe('ProductMultiDimensionalSelectorGuard', () => { const route = new ActivatedRouteSnapshot(); route.params = { productCode: 'testProductCode' }; const product: Product = { code: 'testProductCode', purchasable: true }; - productService.get.and.returnValue(of(product)); + productService.get.mockReturnValue(of(product)); guard.canActivate(route).subscribe((result) => { expect(result).toBe(true); @@ -76,9 +71,9 @@ describe('ProductMultiDimensionalSelectorGuard', () => { purchasable: false, variantOptions: [{ code: 'variantCode', stock: { stockLevel: 10 } }], }; - productService.get.and.returnValue(of(product)); + productService.get.mockReturnValue(of(product)); const urlTree = new UrlTree(); - router.createUrlTree.and.returnValue(urlTree); + router.createUrlTree.mockReturnValue(urlTree); guard.canActivate(route).subscribe((result) => { expect(result).toBe(urlTree); @@ -93,7 +88,7 @@ describe('ProductMultiDimensionalSelectorGuard', () => { purchasable: false, variantOptions: [], }; - productService.get.and.returnValue(of(product)); + productService.get.mockReturnValue(of(product)); guard.canActivate(route).subscribe((result) => { expect(result).toBe(false); @@ -108,10 +103,10 @@ describe('ProductMultiDimensionalSelectorGuard', () => { variantOptions: [{ code: 'variantCode', stock: { stockLevel: 10 } }], }; const variantProduct: Product = { code: 'variantCode' }; - productService.get.and.returnValue(of(variantProduct)); + productService.get.mockReturnValue(of(variantProduct)); const urlTree = new UrlTree(); - router.createUrlTree.and.returnValue(urlTree); - semanticPathService.transform.and.returnValue([ + router.createUrlTree.mockReturnValue(urlTree); + semanticPathService.transform.mockReturnValue([ '/product', 'variantCode', ]); @@ -133,10 +128,10 @@ describe('ProductMultiDimensionalSelectorGuard', () => { variantOptions: [{ code: 'variantCode', stock: { stockLevel: 0 } }], }; const variantProduct: Product = { code: 'variantCode' }; - productService.get.and.returnValue(of(variantProduct)); + productService.get.mockReturnValue(of(variantProduct)); const urlTree = new UrlTree(); - router.createUrlTree.and.returnValue(urlTree); - semanticPathService.transform.and.returnValue([ + router.createUrlTree.mockReturnValue(urlTree); + semanticPathService.transform.mockReturnValue([ '/product', 'variantCode', ]); diff --git a/feature-libs/product-multi-dimensional/selector/components/selector/product-multi-dimensional-selector.component.spec.ts b/feature-libs/product-multi-dimensional/selector/components/selector/product-multi-dimensional-selector.component.spec.ts index 5b88d88bafc..16c81eb42f8 100644 --- a/feature-libs/product-multi-dimensional/selector/components/selector/product-multi-dimensional-selector.component.spec.ts +++ b/feature-libs/product-multi-dimensional/selector/components/selector/product-multi-dimensional-selector.component.spec.ts @@ -18,34 +18,28 @@ import { } from '@spartacus/product-multi-dimensional/selector/core'; import { CurrentProductService } from '@spartacus/storefront'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { ProductMultiDimensionalSelectorComponent } from './product-multi-dimensional-selector.component'; describe('ProductMultiDimensionalSelectorComponent', () => { let component: ProductMultiDimensionalSelectorComponent; let fixture: ComponentFixture; - let mockProductService: jasmine.SpyObj; - let mockRoutingService: jasmine.SpyObj; - let mockMultiDimensionalService: jasmine.SpyObj; - let mockTranslationService: jasmine.SpyObj; - let mockCurrentProductService: jasmine.SpyObj; + let mockProductService: any; + let mockRoutingService: any; + let mockMultiDimensionalService: any; + let mockTranslationService: any; + let mockCurrentProductService: any; beforeEach(async () => { - mockProductService = jasmine.createSpyObj('ProductService', ['get']); - mockRoutingService = jasmine.createSpyObj('RoutingService', ['go']); - mockMultiDimensionalService = jasmine.createSpyObj( - 'ProductMultiDimensionalSelectorService', - ['getVariants'] - ); - mockTranslationService = jasmine.createSpyObj('TranslationService', [ - 'translate', - ]); - mockTranslationService.translate.and.returnValue(of('test translation')); - - mockCurrentProductService = jasmine.createSpyObj('CurrentProductService', [ - 'getProduct', - ]); - mockCurrentProductService.getProduct.and.returnValue( + mockProductService = { get: vi.fn() }; + mockRoutingService = { go: vi.fn() }; + mockMultiDimensionalService = { getVariants: vi.fn() }; + mockTranslationService = { translate: vi.fn() }; + mockTranslationService.translate.mockReturnValue(of('test translation')); + + mockCurrentProductService = { getProduct: vi.fn() }; + mockCurrentProductService.getProduct.mockReturnValue( of({ code: 'productCode', multidimensional: true, @@ -93,8 +87,8 @@ describe('ProductMultiDimensionalSelectorComponent', () => { const variants: VariantCategoryGroup[] = [ { name: 'category1', hasImages: false, variantOptions: [] }, ]; - mockCurrentProductService.getProduct.and.returnValue(of(product)); - mockMultiDimensionalService.getVariants.and.returnValue(variants); + mockCurrentProductService.getProduct.mockReturnValue(of(product)); + mockMultiDimensionalService.getVariants.mockReturnValue(variants); fixture.detectChanges(); @@ -106,7 +100,7 @@ describe('ProductMultiDimensionalSelectorComponent', () => { describe('changeVariant', () => { it('should call routingService.go with the new product', () => { const newProduct = { code: 'newProductCode' } as Product; - mockProductService.get.and.returnValue(of(newProduct)); + mockProductService.get.mockReturnValue(of(newProduct)); component.changeVariant('newProductCode'); @@ -160,7 +154,7 @@ describe('ProductMultiDimensionalSelectorComponent', () => { describe('getCategoryName', () => { it('should return category name with selected value if hasImages is true', () => { - spyOn(component, 'getSelectedValue').and.returnValue('selectedValue'); + vi.spyOn(component, 'getSelectedValue').mockReturnValue('selectedValue'); const category: VariantCategoryGroup = { name: 'CategoryName', @@ -184,7 +178,7 @@ describe('ProductMultiDimensionalSelectorComponent', () => { }); it('should return only category name if selected value is empty', () => { - spyOn(component, 'getSelectedValue').and.returnValue(''); + vi.spyOn(component, 'getSelectedValue').mockReturnValue(''); const category: VariantCategoryGroup = { name: 'CategoryName', @@ -201,20 +195,20 @@ describe('ProductMultiDimensionalSelectorComponent', () => { it('should return true if the code matches selectedProductCode', () => { component.selectedProductCode = 'option1'; const result = component['isSelected']('option1'); - expect(result).toBeTrue(); + expect(result).toBe(true); }); it('should return false if the code does not match selectedProductCode', () => { component.selectedProductCode = 'option1'; const result = component['isSelected']('option2'); - expect(result).toBeFalse(); + expect(result).toBe(false); }); }); describe('onAriaLabel', () => { it('should return the aria label for selected option', () => { - spyOn(component as any, 'isSelected').and.returnValue(true); - mockTranslationService.translate.and.returnValue(of('Selected')); + vi.spyOn(component as any, 'isSelected').mockReturnValue(true); + mockTranslationService.translate.mockReturnValue(of('Selected')); const option = { code: 'option1', @@ -228,8 +222,8 @@ describe('ProductMultiDimensionalSelectorComponent', () => { }); it('should return the aria label for unselected option', () => { - spyOn(component as any, 'isSelected').and.returnValue(false); - mockTranslationService.translate.and.returnValue(of('Variant')); + vi.spyOn(component as any, 'isSelected').mockReturnValue(false); + mockTranslationService.translate.mockReturnValue(of('Variant')); const option = { code: 'option1', diff --git a/feature-libs/product-multi-dimensional/selector/core/services/product-multi-dimensional-selector.service.spec.ts b/feature-libs/product-multi-dimensional/selector/core/services/product-multi-dimensional-selector.service.spec.ts index 4ae0a40188a..bf512728451 100644 --- a/feature-libs/product-multi-dimensional/selector/core/services/product-multi-dimensional-selector.service.spec.ts +++ b/feature-libs/product-multi-dimensional/selector/core/services/product-multi-dimensional-selector.service.spec.ts @@ -3,16 +3,14 @@ import { ProductMultiDimensionalSelectorService } from './product-multi-dimensio import { ProductMultiDimensionalSelectorImagesService } from './product-multi-dimensional-selector-images.service'; import { Product, VariantMatrixElement } from '@spartacus/core'; import { VariantCategoryGroup } from '../model'; +import { vi } from 'vitest'; describe('ProductMultiDimensionalSelectorService', () => { let service: ProductMultiDimensionalSelectorService; - let imagesService: jasmine.SpyObj; + let imagesService: any; beforeEach(() => { - const imagesServiceSpy = jasmine.createSpyObj( - 'ProductMultiDimensionalSelectorImagesService', - ['getVariantOptionImage'] - ); + const imagesServiceSpy = { getVariantOptionImage: vi.fn() }; TestBed.configureTestingModule({ providers: [ @@ -27,7 +25,7 @@ describe('ProductMultiDimensionalSelectorService', () => { service = TestBed.inject(ProductMultiDimensionalSelectorService); imagesService = TestBed.inject( ProductMultiDimensionalSelectorImagesService - ) as jasmine.SpyObj; + ) as any; }); describe('getVariants', () => { @@ -59,7 +57,7 @@ describe('ProductMultiDimensionalSelectorService', () => { categories: [{ code: 'B2C_Blue' }], code: 'Blue', }; - imagesService.getVariantOptionImage.and.returnValue(undefined); + imagesService.getVariantOptionImage.mockReturnValue(undefined); const result = service.getVariants(product); @@ -93,7 +91,7 @@ describe('ProductMultiDimensionalSelectorService', () => { variantOption: { code: 'Blue_code', variantOptionQualifiers: [] }, elements: [], }; - imagesService.getVariantOptionImage.and.returnValue(undefined); + imagesService.getVariantOptionImage.mockReturnValue(undefined); const result = service['createVariantOptionCategory'](element); @@ -108,7 +106,7 @@ describe('ProductMultiDimensionalSelectorService', () => { variantOption: { code: undefined, variantOptionQualifiers: undefined }, elements: [], }; - imagesService.getVariantOptionImage.and.returnValue(undefined); + imagesService.getVariantOptionImage.mockReturnValue(undefined); const result = service['createVariantOptionCategory'](element); diff --git a/feature-libs/product-multi-dimensional/test.ts b/feature-libs/product-multi-dimensional/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/product-multi-dimensional/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/product-multi-dimensional/tsconfig.spec.json b/feature-libs/product-multi-dimensional/tsconfig.spec.json index c2c11dc145e..d52c68cbde6 100644 --- a/feature-libs/product-multi-dimensional/tsconfig.spec.json +++ b/feature-libs/product-multi-dimensional/tsconfig.spec.json @@ -2,11 +2,18 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": ["jasmine", "node"], "module": "preserve", "strict": false, - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/product-multi-dimensional/vitest.config.ts b/feature-libs/product-multi-dimensional/vitest.config.ts new file mode 100644 index 00000000000..3650bb56bb0 --- /dev/null +++ b/feature-libs/product-multi-dimensional/vitest.config.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/product-multi-dimensional`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-product-multi-dimensional.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/product/bulk-pricing/components/bulk-pricing-table/bulk-pricing-table.component.spec.ts b/feature-libs/product/bulk-pricing/components/bulk-pricing-table/bulk-pricing-table.component.spec.ts index 6b84b2c77a0..d6283778dbc 100644 --- a/feature-libs/product/bulk-pricing/components/bulk-pricing-table/bulk-pricing-table.component.spec.ts +++ b/feature-libs/product/bulk-pricing/components/bulk-pricing-table/bulk-pricing-table.component.spec.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { BulkPrice } from '../../core/model/bulk-price.model'; import { BulkPricingTableComponent } from './bulk-pricing-table.component'; @@ -143,7 +144,7 @@ describe('BulkPricingTableComponent', () => { describe('getPrices', () => { it('should call getBulkPrices with a right parameter', () => { - spyOn(bulkPricingService, 'getBulkPrices').and.callThrough(); + vi.spyOn(bulkPricingService, 'getBulkPrices'); component .getPrices() diff --git a/feature-libs/product/future-stock/components/future-stock-accordion/future-stock-accordion.component.spec.ts b/feature-libs/product/future-stock/components/future-stock-accordion/future-stock-accordion.component.spec.ts index 9dcb3a5325d..e088960c8ca 100644 --- a/feature-libs/product/future-stock/components/future-stock-accordion/future-stock-accordion.component.spec.ts +++ b/feature-libs/product/future-stock/components/future-stock-accordion/future-stock-accordion.component.spec.ts @@ -140,9 +140,9 @@ describe('FutureStockAccordionComponent', () => { By.css('.cx-future-stock-accordion-content') ); - expect(stocks.nativeElement.innerText).toEqual( - 'futureStockDropdown.noFutureStocks' - ); + expect( + stocks.nativeElement.textContent?.replace(/\s+/g, ' ').trim() + ).toEqual('futureStockDropdown.noFutureStocks'); }); it('should show mocked future stocks', () => { @@ -159,15 +159,15 @@ describe('FutureStockAccordionComponent', () => { By.css('.cx-future-stock-accordion-content') ); - expect(stocks[0].nativeElement.innerText).toEqual( - '10/11/2020 - futureStockDropdown.quantity 15' - ); - expect(stocks[1].nativeElement.innerText).toEqual( - '11/11/2020 - futureStockDropdown.quantity 20' - ); - expect(stocks[2].nativeElement.innerText).toEqual( - '12/11/2020 - futureStockDropdown.quantity 25' - ); + expect( + stocks[0].nativeElement.textContent?.replace(/\s+/g, ' ').trim() + ).toEqual('10/11/2020 - futureStockDropdown.quantity 15'); + expect( + stocks[1].nativeElement.textContent?.replace(/\s+/g, ' ').trim() + ).toEqual('11/11/2020 - futureStockDropdown.quantity 20'); + expect( + stocks[2].nativeElement.textContent?.replace(/\s+/g, ' ').trim() + ).toEqual('12/11/2020 - futureStockDropdown.quantity 25'); }); }); }); diff --git a/feature-libs/product/future-stock/core/connectors/future-stock.connector.spec.ts b/feature-libs/product/future-stock/core/connectors/future-stock.connector.spec.ts index f9a02968ec7..e7bff00050d 100644 --- a/feature-libs/product/future-stock/core/connectors/future-stock.connector.spec.ts +++ b/feature-libs/product/future-stock/core/connectors/future-stock.connector.spec.ts @@ -1,25 +1,26 @@ import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { of } from 'rxjs'; import { take } from 'rxjs/operators'; import { FutureStockAdapter } from './future-stock.adapter'; import { FutureStockConnector } from './future-stock.connector'; -import createSpy = jasmine.createSpy; const userId = 'userId1'; const productCode = 'productCode1'; const productCodes = 'productCode1, productCode2'; class MockFutureStockAdapter implements Partial { - getFutureStock = createSpy('FutureStockAdapter.getFutureStock').and.callFake( - (productCode: string, userId: string) => + getFutureStock = vi + .fn() + .mockImplementation((productCode: string, userId: string) => of(`getFutureStock-${userId}-${productCode}`) - ); + ); - getFutureStocks = createSpy( - 'FutureStockAdapter.getFutureStocks' - ).and.callFake((productCodes: string, userId: string) => - of(`getFutureStocks-${userId}-${productCodes}`) - ); + getFutureStocks = vi + .fn() + .mockImplementation((productCodes: string, userId: string) => + of(`getFutureStocks-${userId}-${productCodes}`) + ); } describe('FutureStockConnector', () => { diff --git a/feature-libs/product/future-stock/core/services/future-stock.service.spec.ts b/feature-libs/product/future-stock/core/services/future-stock.service.spec.ts index 738f54a1efa..d4874977259 100644 --- a/feature-libs/product/future-stock/core/services/future-stock.service.spec.ts +++ b/feature-libs/product/future-stock/core/services/future-stock.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { OCC_USER_ID_ANONYMOUS, OCC_USER_ID_CURRENT, @@ -9,9 +10,8 @@ import { Observable, of } from 'rxjs'; import { FutureStockConnector } from '../connectors/future-stock.connector'; import { FutureStockService } from './future-stock.service'; -import createSpy = jasmine.createSpy; class MockFutureStockConnector implements Partial { - getFutureStock = createSpy().and.callFake(() => of(mockFutureStocks)); + getFutureStock = vi.fn().mockImplementation(() => of(mockFutureStocks)); } const mockUserId = OCC_USER_ID_CURRENT; diff --git a/feature-libs/product/future-stock/occ/adapters/occ-future-stock-adapter.spec.ts b/feature-libs/product/future-stock/occ/adapters/occ-future-stock-adapter.spec.ts index ff197f64513..2b2030ca2dd 100644 --- a/feature-libs/product/future-stock/occ/adapters/occ-future-stock-adapter.spec.ts +++ b/feature-libs/product/future-stock/occ/adapters/occ-future-stock-adapter.spec.ts @@ -10,12 +10,14 @@ import { ProductFutureStock, ProductFutureStockList, } from '@spartacus/product/future-stock/core'; +import { firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { OccFutureStockAdapter } from './occ-future-stock.adapter'; import { provideHttpClient, withInterceptorsFromDi, } from '@angular/common/http'; +import { vi } from 'vitest'; const userId = '111111'; const productCode = 'code'; @@ -95,9 +97,9 @@ describe('OccFutureStockAdapter', () => { httpMock = TestBed.inject(HttpTestingController); converter = TestBed.inject(ConverterService); - spyOn(converter, 'pipeable').and.callThrough(); - spyOn(converter, 'pipeableMany').and.callThrough(); - spyOn(converter, 'convert').and.callThrough(); + vi.spyOn(converter, 'pipeable'); + vi.spyOn(converter, 'pipeableMany'); + vi.spyOn(converter, 'convert'); }); afterEach(() => { @@ -105,13 +107,13 @@ describe('OccFutureStockAdapter', () => { }); describe('getFutureStock()', () => { - it(' should return future stock', (done) => { + it(' should return future stock', async () => { + let result: any; service .getFutureStock(userId, productCode) .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(futureStockMock); - done(); + .subscribe((r) => { + result = r; }); const mockReq = httpMock.expectOne((req) => { @@ -122,20 +124,21 @@ describe('OccFutureStockAdapter', () => { expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(futureStockMock); + expect(result).toEqual(futureStockMock); expect(converter.pipeable).toHaveBeenCalledWith(FUTURE_STOCK_NORMALIZER); }); - it('should throw error', (done) => { + it('should throw error', async () => { const mockErrorResponse = { status: 400, statusText: 'Bad Request' }; const data = 'Error message'; + let caughtStatus: number; service .getFutureStock(userId, productCode) .pipe(take(1)) .subscribe({ error: (err) => { - expect(err.status).toEqual(mockErrorResponse.status); - done(); + caughtStatus = err.status; }, }); @@ -143,17 +146,18 @@ describe('OccFutureStockAdapter', () => { return req.method === 'GET'; }); mockReq.flush(data, mockErrorResponse); + expect(caughtStatus).toEqual(mockErrorResponse.status); }); }); describe('getFutureStocks()', () => { - it('should return future stocks', (done) => { + it('should return future stocks', async () => { + let result: any; service .getFutureStocks(userId, productCode) .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(futureStockListMock); - done(); + .subscribe((r) => { + result = r; }); const mockReq = httpMock.expectOne((req) => { @@ -164,22 +168,23 @@ describe('OccFutureStockAdapter', () => { expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(futureStockListMock); + expect(result).toEqual(futureStockListMock); expect(converter.pipeable).toHaveBeenCalledWith( FUTURE_STOCK_LIST_NORMALIZER ); }); - it('should throw error', (done) => { + it('should throw error', async () => { const mockErrorResponse = { status: 400, statusText: 'Bad Request' }; const data = 'Error message'; + let caughtStatus: number; service .getFutureStocks(userId, productCode) .pipe(take(1)) .subscribe({ error: (err) => { - expect(err.status).toEqual(mockErrorResponse.status); - done(); + caughtStatus = err.status; }, }); @@ -187,6 +192,7 @@ describe('OccFutureStockAdapter', () => { return req.method === 'GET'; }); mockReq.flush(data, mockErrorResponse); + expect(caughtStatus).toEqual(mockErrorResponse.status); }); }); }); diff --git a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-dialog/product-image-zoom-dialog.component.spec.ts b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-dialog/product-image-zoom-dialog.component.spec.ts index 1a8fe4cf92d..6e0c2dad4f8 100644 --- a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-dialog/product-image-zoom-dialog.component.spec.ts +++ b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-dialog/product-image-zoom-dialog.component.spec.ts @@ -1,5 +1,6 @@ import { Component, Input } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { CxDatePipe, MockDatePipe, @@ -82,7 +83,7 @@ describe('ProductImageZoomDialogComponent', () => { describe('close', () => { beforeEach(() => { - spyOn(component, 'close').and.callThrough(); + vi.spyOn(component, 'close'); }); it('should call close dialog on handleClick', () => { @@ -101,7 +102,7 @@ describe('ProductImageZoomDialogComponent', () => { }); it('should call close dialog with the close reason', () => { - spyOn(launchDialogService, 'closeDialog').and.callThrough(); + vi.spyOn(launchDialogService, 'closeDialog'); component.close('cross clicked'); expect(launchDialogService.closeDialog).toHaveBeenCalledWith( @@ -109,7 +110,7 @@ describe('ProductImageZoomDialogComponent', () => { ); }); it('should call close dialog without the close reason', () => { - spyOn(launchDialogService, 'closeDialog').and.callThrough(); + vi.spyOn(launchDialogService, 'closeDialog'); component.close(); expect(launchDialogService.closeDialog).toHaveBeenCalledWith(''); diff --git a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-product-images/product-image-zoom-product-images.component.spec.ts b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-product-images/product-image-zoom-product-images.component.spec.ts index 82832fc43b5..623dc7e317f 100644 --- a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-product-images/product-image-zoom-product-images.component.spec.ts +++ b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-product-images/product-image-zoom-product-images.component.spec.ts @@ -1,6 +1,6 @@ import { AsyncPipe, NgFor, NgTemplateOutlet } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CxDatePipe, @@ -20,10 +20,11 @@ import { LcpPresence, MediaComponent, } from '@spartacus/storefront'; -import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, firstValueFrom, of } from 'rxjs'; import { take } from 'rxjs/operators'; import { ProductImageZoomTriggerComponent } from '../product-image-zoom-trigger/product-image-zoom-trigger.component'; import { ProductImageZoomProductImagesComponent } from './product-image-zoom-product-images.component'; +import { vi } from 'vitest'; const firstImage = { zoom: { @@ -125,7 +126,7 @@ describe('ProductImageZoomProductImagesComponent', () => { let currentProductService: CurrentProductService; let mockLcpPresence$: BehaviorSubject; - beforeEach(waitForAsync(() => { + beforeEach(async () => { mockLcpPresence$ = new BehaviorSubject(LcpPresence.NO_LCP); TestBed.configureTestingModule({ @@ -168,22 +169,22 @@ describe('ProductImageZoomProductImagesComponent', () => { .compileComponents(); currentProductService = TestBed.inject(CurrentProductService); - })); + }); - beforeEach(waitForAsync(() => { + beforeEach(async () => { fixture = TestBed.createComponent(ProductImageZoomProductImagesComponent); component = fixture.componentInstance; - })); + }); describe('with multiple pictures', () => { - beforeEach(waitForAsync(() => { - spyOn(currentProductService, 'getProduct').and.returnValue( + beforeEach(async () => { + vi.spyOn(currentProductService, 'getProduct').mockReturnValue( of(mockDataWithMultiplePictures) ); fixture = TestBed.createComponent(ProductImageZoomProductImagesComponent); component = fixture.componentInstance; - })); + }); it('should be created', () => { fixture.detectChanges(); @@ -197,21 +198,21 @@ describe('ProductImageZoomProductImagesComponent', () => { expect(result.zoom.url).toEqual('zoom-1.jpg'); }); - it('should have 2 thumbnails', waitForAsync(() => { + it('should have 2 thumbnails', async () => { fixture.detectChanges(); let items: Observable[]; component.thumbs$.subscribe((i) => (items = i)); expect(items.length).toBe(2); - })); + }); - it('should have thumb with url in first product', waitForAsync(() => { + it('should have thumb with url in first product', async () => { fixture.detectChanges(); let thumbs: Observable[]; component.thumbs$.subscribe((i) => (thumbs = i)); let thumb: any; thumbs[0].subscribe((p) => (thumb = p)); expect(thumb.container.thumbnail.url).toEqual('thumb-1.jpg'); - })); + }); describe('UI test', () => { it('should have cx-carousel-scrolling element', () => { @@ -287,7 +288,7 @@ describe('ProductImageZoomProductImagesComponent', () => { describe('with one pictures', () => { beforeEach(() => { - spyOn(currentProductService, 'getProduct').and.returnValue( + vi.spyOn(currentProductService, 'getProduct').mockReturnValue( of(mockDataWithOnePicture) ); @@ -306,11 +307,11 @@ describe('ProductImageZoomProductImagesComponent', () => { expect(result.zoom.url).toEqual('zoom-1.jpg'); }); - it('should not have thumbnails in case there is only one GALLERY image', waitForAsync(() => { + it('should not have thumbnails in case there is only one GALLERY image', async () => { let items: Observable[]; component.thumbs$.subscribe((i) => (items = i)); expect(items.length).toBe(0); - })); + }); describe('(UI test)', () => { it('should not render cx-carousel-scrolling for one GALLERY image', () => { @@ -361,7 +362,7 @@ describe('ProductImageZoomProductImagesComponent', () => { describe('without pictures', () => { beforeEach(() => { - spyOn(currentProductService, 'getProduct').and.returnValue( + vi.spyOn(currentProductService, 'getProduct').mockReturnValue( of(mockDataWitoutPrimaryPictures) ); @@ -409,12 +410,10 @@ describe('ProductImageZoomProductImagesComponent', () => { }); }); - it('should emit new value for expandImage on triggerZoom', (done) => { + it('should emit new value for expandImage on triggerZoom', async () => { component.triggerZoom(true); - component.expandImage.pipe(take(1)).subscribe((value) => { - expect(value).toBeTruthy(); - done(); - }); + const value = await firstValueFrom(component.expandImage); + expect(value).toBeTruthy(); }); }); diff --git a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-thumbnails/product-image-zoom-thumbnails.component.spec.ts b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-thumbnails/product-image-zoom-thumbnails.component.spec.ts index 977fca7e70b..5aa8026c59b 100644 --- a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-thumbnails/product-image-zoom-thumbnails.component.spec.ts +++ b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-thumbnails/product-image-zoom-thumbnails.component.spec.ts @@ -1,8 +1,9 @@ import { Component, EventEmitter, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FeatureDirective } from '@spartacus/core'; import { CarouselComponent } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; +import { vi } from 'vitest'; import { ProductImageZoomThumbnailsComponent } from './product-image-zoom-thumbnails.component'; const firstImage = { @@ -47,7 +48,7 @@ describe('ProductImageZoomThumbnailsComponent', () => { let productImageZoomThumbnailsComponent: ProductImageZoomThumbnailsComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ProductImageZoomThumbnailsComponent], }) @@ -56,7 +57,7 @@ describe('ProductImageZoomThumbnailsComponent', () => { add: { imports: [MockCarouselComponent, MockFeatureDirective] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ProductImageZoomThumbnailsComponent); @@ -71,7 +72,7 @@ describe('ProductImageZoomThumbnailsComponent', () => { describe('openImage', () => { it('should emit event with image and index', () => { - spyOn(productImageZoomThumbnailsComponent.productImage, 'emit'); + vi.spyOn(productImageZoomThumbnailsComponent.productImage, 'emit'); productImageZoomThumbnailsComponent.openImage(firstImage); diff --git a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-trigger/product-image-zoom-trigger.component.spec.ts b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-trigger/product-image-zoom-trigger.component.spec.ts index a8dde3d6573..f04941161f1 100644 --- a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-trigger/product-image-zoom-trigger.component.spec.ts +++ b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-trigger/product-image-zoom-trigger.component.spec.ts @@ -16,6 +16,7 @@ import { import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { ProductImageZoomTriggerComponent } from './product-image-zoom-trigger.component'; @Component({ @@ -75,7 +76,7 @@ describe('ProductImageZoomTriggerComponent', () => { describe('expandImage', () => { beforeEach(() => { - spyOn(launchDialogService, 'launch').and.returnValue( + vi.spyOn(launchDialogService, 'launch').mockReturnValue( of(testDialogComponent) ); }); @@ -90,7 +91,7 @@ describe('ProductImageZoomTriggerComponent', () => { }); it('should call LaunchDialogService clear on close', () => { - spyOn(launchDialogService, 'clear'); + vi.spyOn(launchDialogService, 'clear'); component.triggerZoom(); @@ -100,7 +101,7 @@ describe('ProductImageZoomTriggerComponent', () => { }); it('should destroy component on close', () => { - spyOn(testDialogComponent, 'destroy'); + vi.spyOn(testDialogComponent, 'destroy'); component.triggerZoom(); @@ -110,7 +111,7 @@ describe('ProductImageZoomTriggerComponent', () => { describe('on expandImage set ', () => { it('with true value should call triggerZoom method', () => { - spyOn(component, 'triggerZoom'); + vi.spyOn(component, 'triggerZoom'); fixture.componentInstance.expandImage = true; @@ -118,7 +119,7 @@ describe('ProductImageZoomTriggerComponent', () => { }); it('with false value should not call triggerZoom method', () => { - spyOn(component, 'triggerZoom'); + vi.spyOn(component, 'triggerZoom'); fixture.componentInstance.expandImage = false; diff --git a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-view/product-image-zoom-view.component.spec.ts b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-view/product-image-zoom-view.component.spec.ts index 8b4a8dc8fad..3145557372f 100644 --- a/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-view/product-image-zoom-view.component.spec.ts +++ b/feature-libs/product/image-zoom/components/product-image-zoom/product-image-zoom-view/product-image-zoom-view.component.spec.ts @@ -5,17 +5,12 @@ import { Input, Output, } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CxDatePipe, FeaturesConfigModule, + FeatureToggles, I18nTestingModule, ImageGroup, MockDatePipe, @@ -28,14 +23,20 @@ import { BREAKPOINT, BreakpointService, CurrentProductService, + FeatureDirective, IconComponent, MediaComponent, } from '@spartacus/storefront'; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { EMPTY, Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { ProductImageZoomThumbnailsComponent } from '../product-image-zoom-thumbnails/product-image-zoom-thumbnails.component'; import { ProductImageZoomViewComponent } from './product-image-zoom-view.component'; -import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/feature-toggles/testing'; +import { + MockFeatureTogglesController, + provideMockFeatureToggles, +} from 'core-libs/core/src/features-config/feature-toggles/testing'; const firstImage = { zoom: { @@ -136,9 +137,12 @@ describe('ProductImageZoomViewComponent', () => { providers: [ { provide: CurrentProductService, useClass: MockCurrentProductService }, { provide: BreakpointService, useClass: MockBreakpointService }, - provideMockFeatureToggles({ - a11yKeyboardAccessibleZoom: true, - }), + { + provide: FeatureToggles, + useValue: { + a11yKeyboardAccessibleZoom: true, + }, + }, ], }) .overrideComponent(ProductImageZoomViewComponent, { @@ -146,6 +150,7 @@ describe('ProductImageZoomViewComponent', () => { imports: [ TranslatePipe, CxDatePipe, + FeatureDirective, IconComponent, MediaComponent, ProductImageZoomThumbnailsComponent, @@ -155,6 +160,7 @@ describe('ProductImageZoomViewComponent', () => { imports: [ MockTranslatePipe, MockDatePipe, + MockFeatureDirective, MockIconComponent, MockMediaComponent, MockProductImageZoomThumbnailsComponent, @@ -174,7 +180,7 @@ describe('ProductImageZoomViewComponent', () => { describe('with multiple pictures', () => { beforeEach(() => { - spyOn(currentProductService, 'getProduct').and.returnValue( + vi.spyOn(currentProductService, 'getProduct').mockReturnValue( of(mockDataWithMultiplePictures) ); fixture = TestBed.createComponent(ProductImageZoomViewComponent); @@ -190,19 +196,19 @@ describe('ProductImageZoomViewComponent', () => { expect(result.zoom.url).toEqual('zoom-1.jpg'); }); - it('should have 2 thumbnails', waitForAsync(() => { + it('should have 2 thumbnails', async () => { let items: Observable[]; productImageZoomViewComponent.thumbnails$.subscribe((i) => (items = i)); expect(items.length).toBe(2); - })); + }); - it('should have thumb with url in first product', waitForAsync(() => { + it('should have thumb with url in first product', async () => { let thumbs: Observable[]; productImageZoomViewComponent.thumbnails$.subscribe((i) => (thumbs = i)); let thumb: any; thumbs[0].subscribe((p) => (thumb = p)); expect(thumb.container.thumbnail.url).toEqual('thumb-1.jpg'); - })); + }); it('should zoom on click', () => { const defaultImageElement = fixture.debugElement.query( @@ -227,7 +233,7 @@ describe('ProductImageZoomViewComponent', () => { describe('with one pictures', () => { beforeEach(() => { - spyOn(currentProductService, 'getProduct').and.returnValue( + vi.spyOn(currentProductService, 'getProduct').mockReturnValue( of(mockDataWithOnePicture) ); fixture = TestBed.createComponent(ProductImageZoomViewComponent); @@ -247,16 +253,16 @@ describe('ProductImageZoomViewComponent', () => { expect(result.zoom.url).toEqual('zoom-1.jpg'); }); - it('should not have thumbnails in case there is only one GALLERY image', waitForAsync(() => { + it('should not have thumbnails in case there is only one GALLERY image', async () => { let items: Observable[]; productImageZoomViewComponent.thumbnails$.subscribe((i) => (items = i)); expect(items.length).toBe(0); - })); + }); }); describe('without pictures', () => { beforeEach(() => { - spyOn(currentProductService, 'getProduct').and.returnValue( + vi.spyOn(currentProductService, 'getProduct').mockReturnValue( of(mockDataWithoutPrimaryPictures) ); @@ -426,10 +432,11 @@ describe('ProductImageZoomViewComponent', () => { }); describe('a11y', () => { - it('should refocus on zoomButton after image loads', fakeAsync(() => { + it('should refocus on zoomButton after image loads', async () => { + vi.useFakeTimers(); const mockZoomButton = { nativeElement: { - focus: jasmine.createSpy('focus'), + focus: vi.fn(), }, }; productImageZoomViewComponent.zoomButton = mockZoomButton; @@ -437,10 +444,9 @@ describe('ProductImageZoomViewComponent', () => { productImageZoomViewComponent.zoom(); productImageZoomViewComponent['imageLoaded'].next(true); - setTimeout(() => { - expect(mockZoomButton.nativeElement.focus).toHaveBeenCalled(); - }, 1); - tick(1); - })); + await vi.advanceTimersByTimeAsync(1); + vi.useRealTimers(); + expect(mockZoomButton.nativeElement.focus).toHaveBeenCalled(); + }); }); }); diff --git a/feature-libs/product/karma.conf.js b/feature-libs/product/karma.conf.js deleted file mode 100644 index 583edba0bc9..00000000000 --- a/feature-libs/product/karma.conf.js +++ /dev/null @@ -1,49 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-product.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/product'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 67, - functions: 90, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/product/project.json b/feature-libs/product/project.json index 431f27fb47f..2eb0202c3b8 100644 --- a/feature-libs/product/project.json +++ b/feature-libs/product/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/product/test.ts", - "tsConfig": "feature-libs/product/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/product/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/product/test.ts b/feature-libs/product/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/product/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/product/tsconfig.spec.json b/feature-libs/product/tsconfig.spec.json index c2c11dc145e..03531fe7712 100755 --- a/feature-libs/product/tsconfig.spec.json +++ b/feature-libs/product/tsconfig.spec.json @@ -2,11 +2,27 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": ["jasmine", "node"], "module": "preserve", "strict": false, - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true, + "paths": { + "@spartacus/storefront/testing/mock-feature-directive": [ + "../../core-libs/storefront/shared/test/mock-feature-directive.ts" + ] + } }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": [ + "**/*.ts", + "../../core-libs/core/src/**/*.ts", + "../../core-libs/storefront/**/*.ts" + ] } diff --git a/feature-libs/product/variants/components/guards/product-variants.guard.spec.ts b/feature-libs/product/variants/components/guards/product-variants.guard.spec.ts index f64965a5540..96b575889ae 100644 --- a/feature-libs/product/variants/components/guards/product-variants.guard.spec.ts +++ b/feature-libs/product/variants/components/guards/product-variants.guard.spec.ts @@ -6,8 +6,9 @@ import { RoutingConfig, SemanticPathService, } from '@spartacus/core'; -import { EMPTY, Observable, of } from 'rxjs'; +import { EMPTY, Observable, firstValueFrom, of } from 'rxjs'; import { take } from 'rxjs/operators'; +import { vi } from 'vitest'; import { ProductVariantsGuard } from './product-variants.guard'; const mockPurchasableProduct = { @@ -75,43 +76,36 @@ describe('ProductVariantsGuard', () => { productService = TestBed.inject(ProductService); }); - it('should return true if product is purchasable', (done) => { - spyOn(productService, 'get').and.returnValue(of(mockPurchasableProduct)); + it('should return true if product is purchasable', async () => { + vi.spyOn(productService, 'get').mockReturnValue(of(mockPurchasableProduct)); - guard - .canActivate(activatedRoute) - .pipe(take(1)) - .subscribe((val) => { - expect(val).toBeTruthy(); - done(); - }); + const val = await firstValueFrom( + guard.canActivate(activatedRoute).pipe(take(1)) + ); + expect(val).toBeTruthy(); }); - it('should return url for product variant if product is non-purchasable', (done) => { - spyOn(productService, 'get').and.returnValue(of(mockNonPurchasableProduct)); + it('should return url for product variant if product is non-purchasable', async () => { + vi.spyOn(productService, 'get').mockReturnValue( + of(mockNonPurchasableProduct) + ); - guard - .canActivate(activatedRoute) - .pipe(take(1)) - .subscribe((val) => { - expect(val.toString()).toEqual( - '/product/purchasableTest123/nonPurchasableProduct' - ); - done(); - }); + const val = await firstValueFrom( + guard.canActivate(activatedRoute).pipe(take(1)) + ); + expect(val.toString()).toEqual( + '/product/purchasableTest123/nonPurchasableProduct' + ); }); - it('should return true if no productCode in route parameter (launch from smartedit)', (done) => { + it('should return true if no productCode in route parameter (launch from smartedit)', async () => { const activatedRouteWithoutParams = { params: {}, } as unknown as ActivatedRouteSnapshot; - guard - .canActivate(activatedRouteWithoutParams) - .pipe(take(1)) - .subscribe((val) => { - expect(val).toBeTruthy(); - done(); - }); + const val = await firstValueFrom( + guard.canActivate(activatedRouteWithoutParams).pipe(take(1)) + ); + expect(val).toBeTruthy(); }); }); diff --git a/feature-libs/product/variants/components/product-variants-container/product-variants-container.component.spec.ts b/feature-libs/product/variants/components/product-variants-container/product-variants-container.component.spec.ts index be08b0a5159..5520f698cd8 100644 --- a/feature-libs/product/variants/components/product-variants-container/product-variants-container.component.spec.ts +++ b/feature-libs/product/variants/components/product-variants-container/product-variants-container.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NavigationExtras } from '@angular/router'; import { BaseOption, @@ -94,7 +94,7 @@ describe('ProductVariantsContainerComponent', () => { let component: ProductVariantsContainerComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ProductVariantsContainerComponent], providers: [ @@ -131,7 +131,7 @@ describe('ProductVariantsContainerComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ProductVariantsContainerComponent); diff --git a/feature-libs/product/variants/components/variant-color-selector/product-variant-color-selector.component.spec.ts b/feature-libs/product/variants/components/variant-color-selector/product-variant-color-selector.component.spec.ts index f82a52006bb..b231df949e7 100644 --- a/feature-libs/product/variants/components/variant-color-selector/product-variant-color-selector.component.spec.ts +++ b/feature-libs/product/variants/components/variant-color-selector/product-variant-color-selector.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NavigationExtras } from '@angular/router'; import { BaseOption, @@ -10,6 +10,7 @@ import { VariantType, } from '@spartacus/core'; import { ProductVariantColorSelectorComponent } from './product-variant-color-selector.component'; +import { vi } from 'vitest'; const mockVariant: BaseOption = { selected: { @@ -47,14 +48,14 @@ describe('ProductVariantColorSelectorComponent', () => { let fixture: ComponentFixture; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule, ProductVariantColorSelectorComponent], providers: [{ provide: RoutingService, useClass: MockRoutingService }], }).compileComponents(); routingService = TestBed.inject(RoutingService); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ProductVariantColorSelectorComponent); @@ -68,7 +69,7 @@ describe('ProductVariantColorSelectorComponent', () => { }); it('should go to product given code and name', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); component.changeColor('test1', 'testProduct'); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'product', diff --git a/feature-libs/product/variants/components/variant-size-selector/product-variant-size-selector.component.spec.ts b/feature-libs/product/variants/components/variant-size-selector/product-variant-size-selector.component.spec.ts index 0c8d8992720..81b757daf3b 100644 --- a/feature-libs/product/variants/components/variant-size-selector/product-variant-size-selector.component.spec.ts +++ b/feature-libs/product/variants/components/variant-size-selector/product-variant-size-selector.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NavigationExtras } from '@angular/router'; import { @@ -13,6 +13,7 @@ import { VariantQualifier, } from '@spartacus/core'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { ProductVariantSizeSelectorComponent } from './product-variant-size-selector.component'; class MockTranslationService { @@ -94,7 +95,7 @@ describe('ProductVariantSizeSelectorComponent', () => { let fixture: ComponentFixture; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule, ProductVariantSizeSelectorComponent], providers: [ @@ -107,7 +108,7 @@ describe('ProductVariantSizeSelectorComponent', () => { ], }).compileComponents(); routingService = TestBed.inject(RoutingService); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ProductVariantSizeSelectorComponent); @@ -120,7 +121,7 @@ describe('ProductVariantSizeSelectorComponent', () => { }); it('should send emit', () => { - spyOn(component, 'changeSize').and.stub(); + vi.spyOn(component, 'changeSize').mockImplementation(() => {}); component.changeSize('code'); @@ -128,7 +129,7 @@ describe('ProductVariantSizeSelectorComponent', () => { }); it('should go to product given code', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); component.changeSize('p1'); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'product', diff --git a/feature-libs/product/variants/components/variant-style-selector/product-variant-style-selector.component.spec.ts b/feature-libs/product/variants/components/variant-style-selector/product-variant-style-selector.component.spec.ts index 60ee0843480..d6b49c15ed7 100644 --- a/feature-libs/product/variants/components/variant-style-selector/product-variant-style-selector.component.spec.ts +++ b/feature-libs/product/variants/components/variant-style-selector/product-variant-style-selector.component.spec.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BaseOption, I18nTestingModule, @@ -13,6 +13,7 @@ import { VariantType, } from '@spartacus/core'; import { EMPTY, Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { ProductVariantStyleSelectorComponent } from './product-variant-style-selector.component'; const mockOccBackendUrl = 'https://base.com'; @@ -86,7 +87,7 @@ describe('ProductVariantStyleSelectorComponent', () => { let fixture: ComponentFixture; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ I18nTestingModule, @@ -105,7 +106,7 @@ describe('ProductVariantStyleSelectorComponent', () => { { provide: RoutingService, useClass: MockRoutingService }, ], }).compileComponents(); - })); + }); describe('Empty config scenario', () => { beforeEach(() => { @@ -163,7 +164,7 @@ describe('ProductVariantStyleSelectorComponent', () => { }); it('should naviagate to product on changeStyle', () => { - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); component.changeStyle('test123'); expect(routingService.go).toHaveBeenCalled(); diff --git a/feature-libs/product/variants/root/components/variant-style-icons/product-variant-style-icons.component.spec.ts b/feature-libs/product/variants/root/components/variant-style-icons/product-variant-style-icons.component.spec.ts index a8e4c38ee87..70d4f49b2b4 100644 --- a/feature-libs/product/variants/root/components/variant-style-icons/product-variant-style-icons.component.spec.ts +++ b/feature-libs/product/variants/root/components/variant-style-icons/product-variant-style-icons.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { OccConfig, VariantOption, VariantQualifier } from '@spartacus/core'; import { ProductVariantStyleIconsComponent } from './product-variant-style-icons.component'; @@ -40,7 +40,7 @@ describe('ProductVariantStyleIconsComponent', () => { let component: ProductVariantStyleIconsComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ProductVariantStyleIconsComponent], providers: [ @@ -50,7 +50,7 @@ describe('ProductVariantStyleIconsComponent', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ProductVariantStyleIconsComponent); diff --git a/feature-libs/product/vitest.config.ts b/feature-libs/product/vitest.config.ts new file mode 100644 index 00000000000..8bdfa9b204c --- /dev/null +++ b/feature-libs/product/vitest.config.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + '@spartacus/storefront/testing/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + 'core-libs/core/src/features-config/feature-toggles/testing': `${root}/core-libs/core/src/features-config/feature-toggles/testing/index.ts`, + }, + }, + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/product`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-product.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/qualtrics/components/qualtrics-loader/qualtrics-loader.service.spec.ts b/feature-libs/qualtrics/components/qualtrics-loader/qualtrics-loader.service.spec.ts index b029705909f..345a681d863 100644 --- a/feature-libs/qualtrics/components/qualtrics-loader/qualtrics-loader.service.spec.ts +++ b/feature-libs/qualtrics/components/qualtrics-loader/qualtrics-loader.service.spec.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { ScriptLoader, WindowRef } from '@spartacus/core'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { QualtricsLoaderService, QUALTRICS_EVENT_NAME, @@ -24,7 +25,7 @@ const mockedWindowRef = { addEventListener: (event, listener) => { eventListener[event] = listener; }, - removeEventListener: jasmine.createSpy('removeEventListener'), + removeEventListener: vi.fn(), QSI: mockQsiJsApi, }, document: { @@ -76,17 +77,25 @@ describe('QualtricsLoaderService', () => { scriptLoader = TestBed.inject(ScriptLoader); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should be created', () => { expect(service).toBeTruthy(); }); describe('Consume Qualtrics API', () => { - let qsiRun: jasmine.Spy; - let qsiUnload: jasmine.Spy; + let qsiRun: ReturnType; + let qsiUnload: ReturnType; beforeEach(() => { - qsiRun = spyOn(winRef.nativeWindow['QSI'].API, 'run').and.stub(); - qsiUnload = spyOn(winRef.nativeWindow['QSI'].API, 'unload').and.stub(); + qsiRun = vi + .spyOn(winRef.nativeWindow['QSI'].API, 'run') + .mockImplementation(() => {}); + qsiUnload = vi + .spyOn(winRef.nativeWindow['QSI'].API, 'unload') + .mockImplementation(() => {}); }); it('should not load Qualtrics when the qsi_js_loaded event is not triggered', () => { @@ -112,7 +121,9 @@ describe('QualtricsLoaderService', () => { }); it('should unload when a script is alread in the DOM', () => { - spyOn(winRef.document, 'querySelector').and.returnValue({} as Element); + vi.spyOn(winRef.document, 'querySelector').mockReturnValue( + {} as Element + ); service.addScript(mockScript); expect(qsiUnload).toHaveBeenCalled(); }); @@ -121,7 +132,7 @@ describe('QualtricsLoaderService', () => { describe('addScript()', () => { beforeEach(() => { - spyOn(scriptLoader, 'embedScript').and.callThrough(); + vi.spyOn(scriptLoader, 'embedScript'); loadQsi(); }); @@ -134,7 +145,7 @@ describe('QualtricsLoaderService', () => { it('should not add the same script twice', () => { // simulate script has been added - spyOn(winRef.document, 'querySelector').and.returnValue({} as Element); + vi.spyOn(winRef.document, 'querySelector').mockReturnValue({} as Element); service.addScript(mockScript); expect(scriptLoader.embedScript).not.toHaveBeenCalled(); }); @@ -143,7 +154,7 @@ describe('QualtricsLoaderService', () => { describe('custom service', () => { it('should invoke custom data collector', () => { const customService = TestBed.inject(CustomQualtricsLoaderService); - spyOn(customService, 'collectData').and.callThrough(); + vi.spyOn(customService, 'collectData'); eventListener[QUALTRICS_EVENT_NAME](new Event(QUALTRICS_EVENT_NAME)); diff --git a/feature-libs/qualtrics/components/qualtrics-loader/qualtrics.component.spec.ts b/feature-libs/qualtrics/components/qualtrics-loader/qualtrics.component.spec.ts index 61502c33e4c..9fab520e234 100644 --- a/feature-libs/qualtrics/components/qualtrics-loader/qualtrics.component.spec.ts +++ b/feature-libs/qualtrics/components/qualtrics-loader/qualtrics.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { QualtricsConfig } from './config/qualtrics-config'; import { QualtricsLoaderService } from './qualtrics-loader.service'; import { QualtricsComponent } from './qualtrics.component'; @@ -33,7 +34,7 @@ describe('QualtricsComponent', () => { function stubSeviceAndCreateComponent() { service = TestBed.inject(QualtricsLoaderService); - spyOn(service, 'addScript').and.stub(); + vi.spyOn(service, 'addScript').mockImplementation(() => {}); fixture = TestBed.createComponent(QualtricsComponent); component = fixture.componentInstance; diff --git a/feature-libs/qualtrics/karma.conf.js b/feature-libs/qualtrics/karma.conf.js deleted file mode 100644 index 4cac7f8c2db..00000000000 --- a/feature-libs/qualtrics/karma.conf.js +++ /dev/null @@ -1,49 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-qualtrics.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/qualtrics'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 80, - lines: 80, - branches: 60, - functions: 80, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/qualtrics/project.json b/feature-libs/qualtrics/project.json index 2de4765aa00..024c2e02411 100644 --- a/feature-libs/qualtrics/project.json +++ b/feature-libs/qualtrics/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/qualtrics/test.ts", - "tsConfig": "feature-libs/qualtrics/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/qualtrics/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/qualtrics/test.ts b/feature-libs/qualtrics/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/qualtrics/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/qualtrics/tsconfig.spec.json b/feature-libs/qualtrics/tsconfig.spec.json index 34d8415e3a6..d52c68cbde6 100644 --- a/feature-libs/qualtrics/tsconfig.spec.json +++ b/feature-libs/qualtrics/tsconfig.spec.json @@ -4,9 +4,16 @@ "outDir": "../../out-tsc/spec", "module": "preserve", "strict": false, - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/qualtrics/vitest.config.ts b/feature-libs/qualtrics/vitest.config.ts new file mode 100644 index 00000000000..0c0794d1c66 --- /dev/null +++ b/feature-libs/qualtrics/vitest.config.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/qualtrics`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-qualtrics.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/quote/components/cart-guard/quote-cart.guard.spec.ts b/feature-libs/quote/components/cart-guard/quote-cart.guard.spec.ts index c205442b3a9..63d65972fe5 100644 --- a/feature-libs/quote/components/cart-guard/quote-cart.guard.spec.ts +++ b/feature-libs/quote/components/cart-guard/quote-cart.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { @@ -6,11 +7,10 @@ import { RoutingService, SemanticPathService, } from '@spartacus/core'; -import { of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { QuoteCartService } from '../../core/services/quote-cart.service'; import { QUOTE_CODE } from '../../core/testing/quote-test-utils'; import { QuoteCartGuard } from './quote-cart.guard'; -import createSpy = jasmine.createSpy; const URL_PARTS = ['/', 'my-account', 'quote', QUOTE_CODE]; @@ -56,7 +56,7 @@ const routerStateCart: RouterState = { }; class MockRoutingService { - go = createSpy(); + go = vi.fn(); getRouterState() { return of(routerState); } @@ -105,52 +105,42 @@ describe('QuoteCartGuard', () => { }); describe('canActivate', () => { - it('should return true if quote cart is not present', (done) => { - classUnderTest.canActivate().subscribe((canActive) => { - expect(canActive).toBe(true); - done(); - }); + it('should return true if quote cart is not present', async () => { + const canActive = await firstValueFrom(classUnderTest.canActivate()); + expect(canActive).toBe(true); }); - it('should redirect if quote cart is present', (done) => { + it('should redirect if quote cart is present', async () => { isQuoteCartActive = true; quoteId = QUOTE_CODE; - classUnderTest.canActivate().subscribe((canActive) => { - expect(canActive.toString()).toContain(QUOTE_CODE); - done(); - }); + const canActive = await firstValueFrom(classUnderTest.canActivate()); + expect(canActive.toString()).toContain(QUOTE_CODE); }); - it('should allow a navigation to checkout if service allows it', (done) => { + it('should allow a navigation to checkout if service allows it', async () => { isQuoteCartActive = true; checkoutAllowed = true; quoteId = QUOTE_CODE; - classUnderTest.canActivate().subscribe((result) => { - expect(result).toBe(true); - done(); - }); + const result = await firstValueFrom(classUnderTest.canActivate()); + expect(result).toBe(true); }); - it('should allow a navigation to checkout if service allows it, current state is checkout and nextState is undefined', (done) => { + it('should allow a navigation to checkout if service allows it, current state is checkout and nextState is undefined', async () => { isQuoteCartActive = true; checkoutAllowed = true; routerState = routerStateCheckoutWoNextState; quoteId = QUOTE_CODE; - classUnderTest.canActivate().subscribe((result) => { - expect(result).toBe(true); - done(); - }); + const result = await firstValueFrom(classUnderTest.canActivate()); + expect(result).toBe(true); }); - it('should not allow a navigation to cart if service allows checkout', (done) => { + it('should not allow a navigation to cart if service allows checkout', async () => { isQuoteCartActive = true; checkoutAllowed = true; routerState = routerStateCart; quoteId = QUOTE_CODE; - classUnderTest.canActivate().subscribe((result) => { - expect(result.toString()).toContain(QUOTE_CODE); - done(); - }); + const result = await firstValueFrom(classUnderTest.canActivate()); + expect(result.toString()).toContain(QUOTE_CODE); }); }); }); diff --git a/feature-libs/quote/components/comments/quote-comments.component.spec.ts b/feature-libs/quote/components/comments/quote-comments.component.spec.ts index dbeac1b7fed..d6477d7110e 100644 --- a/feature-libs/quote/components/comments/quote-comments.component.spec.ts +++ b/feature-libs/quote/components/comments/quote-comments.component.spec.ts @@ -1,11 +1,6 @@ import { Component, DOCUMENT, Input } from '@angular/core'; -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, - waitForAsync, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { OrderEntry } from '@spartacus/cart/base/root'; import { EventService, I18nTestingModule } from '@spartacus/core'; import { QuoteDetailsReloadQueryEvent } from '@spartacus/quote/core'; @@ -60,7 +55,7 @@ describe('QuoteCommentsComponent', () => { let quote: Quote; - beforeEach(waitForAsync(() => { + beforeEach(async () => { initTestData(); initMocks(); TestBed.configureTestingModule({ @@ -89,26 +84,29 @@ describe('QuoteCommentsComponent', () => { add: { imports: [MockCxMessagingComponent, MockCxIconComponent] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(QuoteCommentsComponent); htmlElem = fixture.nativeElement; component = fixture.componentInstance; - fixture.detectChanges(); - spyOn(component.commentsComponent, 'resetForm'); - - mockQuoteItemsComponentService = jasmine.createSpyObj( - 'QuoteItemsComponentService', - ['setQuoteEntriesExpanded', 'getQuoteEntriesExpanded'] - ); - asSpy( - mockQuoteItemsComponentService.getQuoteEntriesExpanded - ).and.returnValue(of(true)); + mockQuoteItemsComponentService = { + setQuoteEntriesExpanded: vi.fn(), + getQuoteEntriesExpanded: vi.fn(), + } as any; + ( + mockQuoteItemsComponentService.getQuoteEntriesExpanded as vi.Mock + ).mockReturnValue(of(true)); quoteItemsComponentService = TestBed.inject(QuoteItemsComponentService); }); + /** Renders the component and sets up ViewChild spies. Call at the start of each test that needs the DOM or ViewChild. */ + function renderComponent() { + fixture.detectChanges(); + vi.spyOn(component.commentsComponent, 'resetForm'); + } + function initTestData() { quote = createEmptyQuote(); quote.code = QUOTE_CODE; @@ -118,21 +116,18 @@ describe('QuoteCommentsComponent', () => { } function initMocks() { - quoteFacade = jasmine.createSpyObj('QuoteFacade', [ - 'getQuoteDetails', - 'addQuoteComment', - ]); - asSpy(quoteFacade.getQuoteDetails).and.returnValue(of(quote)); - asSpy(quoteFacade.addQuoteComment).and.returnValue(of({})); - - eventService = jasmine.createSpyObj('EventService', ['dispatch']); - } - - function asSpy(f: any) { - return f; + quoteFacade = { + getQuoteDetails: vi.fn(), + addQuoteComment: vi.fn(), + } as any; + (quoteFacade.getQuoteDetails as vi.Mock).mockReturnValue(of(quote)); + (quoteFacade.addQuoteComment as vi.Mock).mockReturnValue(of({})); + + eventService = { dispatch: vi.fn() } as any; } it('should create', () => { + renderComponent(); expect(component).toBeTruthy(); }); @@ -162,6 +157,7 @@ describe('QuoteCommentsComponent', () => { }); it('should render the messaging section by default', () => { + renderComponent(); CommonQuoteTestUtilsService.expectElementPresent( expect, htmlElem, @@ -171,6 +167,7 @@ describe('QuoteCommentsComponent', () => { describe('clickToggle', () => { it('should collapse the comments area when clicking the toggle', () => { + renderComponent(); CommonQuoteTestUtilsService.clickToggle(htmlElem, false); fixture.detectChanges(); CommonQuoteTestUtilsService.expectElementNotPresent( @@ -181,6 +178,7 @@ describe('QuoteCommentsComponent', () => { }); it('should toggle the comments on enter', () => { + renderComponent(); CommonQuoteTestUtilsService.clickToggle(htmlElem, true); fixture.detectChanges(); CommonQuoteTestUtilsService.expectElementNotPresent( @@ -191,6 +189,7 @@ describe('QuoteCommentsComponent', () => { }); it('should expand the comments area when clicking the toggle', () => { + renderComponent(); component.expandComments = false; CommonQuoteTestUtilsService.clickToggle(htmlElem, false); CommonQuoteTestUtilsService.expectElementPresent( @@ -393,6 +392,7 @@ describe('QuoteCommentsComponent', () => { describe('onSend', () => { it('should add a header quote comment with the given text', () => { + renderComponent(); component.onSend( { message: 'test comment', itemId: ALL_PRODUCTS_ID }, QUOTE_CODE @@ -406,6 +406,7 @@ describe('QuoteCommentsComponent', () => { ); }); it('should add a item quote comment with the given text', () => { + renderComponent(); component.onSend({ message: 'test comment', itemId: '3' }, QUOTE_CODE); expect(quoteFacade.addQuoteComment).toHaveBeenCalledWith( QUOTE_CODE, @@ -416,6 +417,7 @@ describe('QuoteCommentsComponent', () => { ); }); it('should refresh the quote to display the just added comment', () => { + renderComponent(); component.onSend( { message: 'test comment', itemId: ALL_PRODUCTS_ID }, QUOTE_CODE @@ -426,6 +428,7 @@ describe('QuoteCommentsComponent', () => { ); }); it('should reset message input text', () => { + renderComponent(); component.onSend( { message: 'test comment', itemId: ALL_PRODUCTS_ID }, QUOTE_CODE @@ -434,7 +437,8 @@ describe('QuoteCommentsComponent', () => { expect(component.messagingConfigs.newMessagePlaceHolder).toBeUndefined(); }); it('should handle errors', () => { - asSpy(quoteFacade.addQuoteComment).and.returnValue( + renderComponent(); + (quoteFacade.addQuoteComment as vi.Mock).mockReturnValue( throwError(new Error('test error')) ); component.onSend( @@ -457,37 +461,43 @@ describe('QuoteCommentsComponent', () => { aTagProduct2 = createElementMock('Product 2'); const mockedATags = [aTagProduct1, aTagProduct2]; const document = TestBed.inject(DOCUMENT); - spyOn(document, 'getElementsByTagName').and.returnValue(mockedATags); + vi.spyOn(document, 'getElementsByTagName').mockReturnValue( + mockedATags + ); quoteItemsComponentService = TestBed.inject(QuoteItemsComponentService); }); function createElementMock(textContent: string) { const elem = { textContent: textContent, scrollIntoView: function () {} }; - spyOn(elem, 'scrollIntoView'); + vi.spyOn(elem, 'scrollIntoView'); return elem; } - it('should expand cart and call scrollIntoView on the corresponding cart item in the document', fakeAsync(() => { + it('should expand cart and call scrollIntoView on the corresponding cart item in the document', async () => { + vi.useFakeTimers(); component.onItemClicked({ item: { id: 'P2', name: 'Product 2' } }); expect( quoteItemsComponentService.setQuoteEntriesExpanded ).toHaveBeenCalledWith(true); - tick(); //because of delay(0) + await vi.advanceTimersByTimeAsync(0); //because of delay(0) + vi.useRealTimers(); expect(aTagProduct1.scrollIntoView).not.toHaveBeenCalled(); expect(aTagProduct2.scrollIntoView).toHaveBeenCalledWith({ block: 'center', }); - })); + }); - it('should only expand the cart but not scroll if the target item is not found in the document', fakeAsync(() => { + it('should only expand the cart but not scroll if the target item is not found in the document', async () => { + vi.useFakeTimers(); component.onItemClicked({ item: { id: 'P3', name: 'Product 3' } }); expect( quoteItemsComponentService.setQuoteEntriesExpanded ).toHaveBeenCalledWith(true); - tick(); //because of delay(0) + await vi.advanceTimersByTimeAsync(0); //because of delay(0) + vi.useRealTimers(); expect(aTagProduct1.scrollIntoView).not.toHaveBeenCalled(); expect(aTagProduct2.scrollIntoView).not.toHaveBeenCalled(); - })); + }); }); describe('prepareMessageEvents', () => { @@ -520,6 +530,7 @@ describe('QuoteCommentsComponent', () => { describe('Accessibility', () => { it("should contain 'div' HTML element with 'role' attribute that indicates the role for this element", () => { + renderComponent(); const element = CommonQuoteTestUtilsService.getElementByClassNameOrTreeOrder( htmlElem, @@ -537,6 +548,7 @@ describe('QuoteCommentsComponent', () => { }); it("should contain 'div' HTML element with 'aria-label' attribute that indicates the text for this element", () => { + renderComponent(); const element = CommonQuoteTestUtilsService.getElementByClassNameOrTreeOrder( htmlElem, diff --git a/feature-libs/quote/components/confirm-dialog/quote-confirm-dialog.component.spec.ts b/feature-libs/quote/components/confirm-dialog/quote-confirm-dialog.component.spec.ts index 68f0c47976b..94ad7508cc2 100644 --- a/feature-libs/quote/components/confirm-dialog/quote-confirm-dialog.component.spec.ts +++ b/feature-libs/quote/components/confirm-dialog/quote-confirm-dialog.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { Component, Directive, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CxDatePipe, LanguageService, @@ -81,7 +82,7 @@ describe('QuoteConfirmDialogComponent', () => { data$ = dialogDataSender; } - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [QuoteConfirmDialogComponent], providers: [ @@ -108,7 +109,7 @@ describe('QuoteConfirmDialogComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { dialogDataSender = new BehaviorSubject({ @@ -119,7 +120,7 @@ describe('QuoteConfirmDialogComponent', () => { component = fixture.componentInstance; launchDialogService = TestBed.inject(LaunchDialogService); cxDatePipe = TestBed.inject(CxDatePipe); - spyOn(launchDialogService, 'closeDialog'); + vi.spyOn(launchDialogService, 'closeDialog'); component.ngOnInit(); fixture.detectChanges(); }); diff --git a/feature-libs/quote/components/header/buyer-edit/quote-header-buyer-edit.component.spec.ts b/feature-libs/quote/components/header/buyer-edit/quote-header-buyer-edit.component.spec.ts index ade80113b66..864283916d7 100644 --- a/feature-libs/quote/components/header/buyer-edit/quote-header-buyer-edit.component.spec.ts +++ b/feature-libs/quote/components/header/buyer-edit/quote-header-buyer-edit.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; @@ -36,13 +37,14 @@ describe('QuoteHeaderBuyerEditComponent', () => { component = fixture.componentInstance; component.content = mockCard; component.enablePurchaseOrderNumber = true; - fixture.detectChanges(); + // No detectChanges() here — tests that mutate form state call it themselves - spyOn(component.saveCard, 'emit').and.callThrough(); - spyOn(component.cancelCard, 'emit').and.callThrough(); + vi.spyOn(component.saveCard, 'emit'); + vi.spyOn(component.cancelCard, 'emit'); }); it('should create and render component accordingly', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); CommonQuoteTestUtilsService.expectElementPresent( @@ -165,6 +167,7 @@ describe('QuoteHeaderBuyerEditComponent', () => { describe('handle action events', () => { it('should emit cancel event', () => { + fixture.detectChanges(); const cancelButton = CommonQuoteTestUtilsService.getHTMLElement( htmlElem, 'button.btn-tertiary' @@ -174,6 +177,7 @@ describe('QuoteHeaderBuyerEditComponent', () => { }); it('should emit edit event for disabling edit mode', () => { + fixture.detectChanges(); const saveButton = CommonQuoteTestUtilsService.getHTMLElement( htmlElem, 'button.btn-secondary' @@ -184,6 +188,7 @@ describe('QuoteHeaderBuyerEditComponent', () => { it('should emit edit event with an edited name and disabling edit mode', () => { const newTextForTitle1: any = 'New title for name'; + fixture.detectChanges(); component.editForm.get('name')?.setValue(newTextForTitle1); component.editForm.get('name')?.markAsDirty(); fixture.detectChanges(); @@ -193,8 +198,7 @@ describe('QuoteHeaderBuyerEditComponent', () => { ); saveButton.click(); expect(component.saveCard.emit).toHaveBeenCalled(); - let arg: any = (component.saveCard.emit as any).calls.mostRecent() - .args[0]; + const arg: any = (component.saveCard.emit as any).mock.lastCall[0]; expect(arg.name).toEqual(newTextForTitle1); }); @@ -202,7 +206,7 @@ describe('QuoteHeaderBuyerEditComponent', () => { const newTextForTitle1: any = 'New title for name'; const newTextForTitle2: any = 'Here could be found a long description'; const newPoNumber: any = 'PO67890'; - component.ngOnInit(); + fixture.detectChanges(); component.editForm.get('name')?.setValue(newTextForTitle1); component.editForm.get('name')?.markAsDirty(); component.editForm.get('description')?.setValue(newTextForTitle2); @@ -216,8 +220,7 @@ describe('QuoteHeaderBuyerEditComponent', () => { ); saveButton.click(); expect(component.saveCard.emit).toHaveBeenCalled(); - let arg: any = (component.saveCard.emit as any).calls.mostRecent() - .args[0]; + const arg: any = (component.saveCard.emit as any).mock.lastCall[0]; expect(arg.name).toEqual(newTextForTitle1); expect(arg.description).toEqual(newTextForTitle2); expect(arg.purchaseOrderNumber).toEqual(newPoNumber); diff --git a/feature-libs/quote/components/header/overview/quote-header-overview.component.spec.ts b/feature-libs/quote/components/header/overview/quote-header-overview.component.spec.ts index 43e0915f85d..7007cdda330 100644 --- a/feature-libs/quote/components/header/overview/quote-header-overview.component.spec.ts +++ b/feature-libs/quote/components/header/overview/quote-header-overview.component.spec.ts @@ -1,5 +1,6 @@ import { Component, Input, Type } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { EventService, I18nTestingModule, @@ -93,7 +94,7 @@ describe('QuoteHeaderOverviewComponent', () => { let eventService: EventService; let quoteUIConfig: QuoteUIConfig; - beforeEach(waitForAsync(() => { + beforeEach(async () => { initMocks(); TestBed.configureTestingModule({ imports: [CardModule, QuoteHeaderOverviewComponent], @@ -130,21 +131,20 @@ describe('QuoteHeaderOverviewComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(QuoteHeaderOverviewComponent); htmlElem = fixture.nativeElement; component = fixture.componentInstance; - - fixture.detectChanges(); + // No detectChanges() here — tests that mutate observables call it themselves quoteFacade = TestBed.inject(QuoteFacade as Type); - spyOn(quoteFacade, 'editQuote').and.callThrough(); + vi.spyOn(quoteFacade, 'editQuote'); }); function initMocks() { - eventService = jasmine.createSpyObj('eventService', ['dispatch']); + eventService = { dispatch: vi.fn() } as any; quoteUIConfig = { quote: { truncateCardTileContentAfterNumChars: 30 }, @@ -152,11 +152,13 @@ describe('QuoteHeaderOverviewComponent', () => { } it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); describe('rendering', () => { it('should render basic component framework accordingly', () => { + fixture.detectChanges(); CommonQuoteTestUtilsService.expectElementPresent( expect, htmlElem, @@ -199,6 +201,7 @@ describe('QuoteHeaderOverviewComponent', () => { }); it('should render component with deactivated edit mode', () => { + fixture.detectChanges(); CommonQuoteTestUtilsService.expectElementPresent( expect, htmlElem, @@ -340,11 +343,13 @@ describe('QuoteHeaderOverviewComponent', () => { describe('handle actions', () => { it('should handle cancel action', () => { + fixture.detectChanges(); component.cancel(); expect(component.editMode).toBe(false); }); it('should handle edit action', () => { + fixture.detectChanges(); const editEvent: SaveEvent = { name: 'new name', description: 'New Description', @@ -365,6 +370,7 @@ describe('QuoteHeaderOverviewComponent', () => { }); it('should set edit mode to the opposite', () => { + fixture.detectChanges(); expect(component.editMode).toBe(false); component.toggleEditMode(); expect(component.editMode).toBe(true); @@ -570,6 +576,7 @@ describe('QuoteHeaderOverviewComponent', () => { describe('Accessibility', () => { it("should contain 'div' HTML element with 'role' attribute that indicates the role for this element", () => { + fixture.detectChanges(); const element = CommonQuoteTestUtilsService.getElementByClassNameOrTreeOrder( htmlElem, @@ -587,6 +594,7 @@ describe('QuoteHeaderOverviewComponent', () => { }); it("should contain 'div' HTML element with 'aria-label' attribute that indicates the text for this element", () => { + fixture.detectChanges(); const element = CommonQuoteTestUtilsService.getElementByClassNameOrTreeOrder( htmlElem, diff --git a/feature-libs/quote/components/items/quote-items.component.service.spec.ts b/feature-libs/quote/components/items/quote-items.component.service.spec.ts index 4f2b8434810..be2bf14586d 100644 --- a/feature-libs/quote/components/items/quote-items.component.service.spec.ts +++ b/feature-libs/quote/components/items/quote-items.component.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { AbstractOrderType, @@ -165,7 +166,7 @@ describe('QuoteItemsComponentService', () => { }); it('should load saved cart', () => { - spyOn(multiCartFacade, 'loadCart'); + vi.spyOn(multiCartFacade, 'loadCart'); mockQuoteDetails$.next(quote); classUnderTest.retrieveQuoteEntries().subscribe().unsubscribe(); expect(multiCartFacade.loadCart).toHaveBeenCalled(); @@ -188,7 +189,7 @@ describe('QuoteItemsComponentService', () => { }); it('should not load an additional cart', () => { - spyOn(multiCartFacade, 'loadCart'); + vi.spyOn(multiCartFacade, 'loadCart'); mockQuoteDetails$.next(quoteWoCartId); classUnderTest.retrieveQuoteEntries().subscribe().unsubscribe(); expect(multiCartFacade.loadCart).toHaveBeenCalledTimes(0); @@ -211,7 +212,7 @@ describe('QuoteItemsComponentService', () => { }); it('should not load an additional cart', () => { - spyOn(multiCartFacade, 'loadCart'); + vi.spyOn(multiCartFacade, 'loadCart'); mockQuoteDetails$.next(quoteEditable); classUnderTest.retrieveQuoteEntries().subscribe().unsubscribe(); expect(multiCartFacade.loadCart).toHaveBeenCalledTimes(0); @@ -219,7 +220,7 @@ describe('QuoteItemsComponentService', () => { }); it('should load saved cart if quote is attached to cart and not editable', () => { - spyOn(multiCartFacade, 'loadCart'); + vi.spyOn(multiCartFacade, 'loadCart'); mockQuoteDetails$.next(quote); classUnderTest.retrieveQuoteEntries().subscribe().unsubscribe(); expect(multiCartFacade.loadCart).toHaveBeenCalled(); diff --git a/feature-libs/quote/components/items/quote-items.component.spec.ts b/feature-libs/quote/components/items/quote-items.component.spec.ts index 041ae7eae6b..47fb482a64a 100644 --- a/feature-libs/quote/components/items/quote-items.component.spec.ts +++ b/feature-libs/quote/components/items/quote-items.component.spec.ts @@ -1,5 +1,6 @@ import { Directive, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { AbstractOrderContextModule } from '@spartacus/cart/base/components'; import { AbstractOrderType } from '@spartacus/cart/base/root'; import { @@ -35,7 +36,7 @@ describe('QuoteItemsComponent', () => { let eventService: EventService; let quoteItemsComponentService: QuoteItemsComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { initMocks(); TestBed.configureTestingModule({ imports: [AbstractOrderContextModule, QuoteItemsComponent], @@ -59,31 +60,33 @@ describe('QuoteItemsComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(QuoteItemsComponent); htmlElem = fixture.nativeElement; component = fixture.componentInstance; component.showCart$ = of(true); - fixture.detectChanges(); + // detectChanges() is called per-test to avoid NG0100 when tests mutate observables }); function initMocks() { - eventService = jasmine.createSpyObj('EventService', ['get', 'dispatch']); - quoteItemsComponentService = jasmine.createSpyObj( - 'QuoteItemsComponentService', - [ - 'setQuoteEntriesExpanded', - 'getQuoteEntriesExpanded', - 'retrieveQuoteEntries', - ] - ); - asSpy(eventService.get).and.returnValue(EMPTY); - asSpy(quoteItemsComponentService.getQuoteEntriesExpanded).and.returnValue( - true - ); - asSpy(quoteItemsComponentService.retrieveQuoteEntries).and.returnValue( + eventService = { + get: vi.fn(), + dispatch: vi.fn(), + } as any; + quoteItemsComponentService = { + setQuoteEntriesExpanded: vi.fn(), + getQuoteEntriesExpanded: vi.fn(), + retrieveQuoteEntries: vi.fn(), + } as any; + (eventService.get as vi.Mock).mockReturnValue(EMPTY); + ( + quoteItemsComponentService.getQuoteEntriesExpanded as vi.Mock + ).mockReturnValue(true); + ( + quoteItemsComponentService.retrieveQuoteEntries as vi.Mock + ).mockReturnValue( of({ entries: quote.entries, readOnly: true, @@ -93,18 +96,16 @@ describe('QuoteItemsComponent', () => { ); } - function asSpy(f: any) { - return f; - } - describe('Initialization', () => { it('should create the component', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); }); describe('Ghost animation', () => { it('should not be present in case quote items data is provided', () => { + fixture.detectChanges(); CommonQuoteTestUtilsService.expectElementNotPresent( expect, htmlElem, @@ -200,6 +201,7 @@ describe('QuoteItemsComponent', () => { describe('onToggleShowOrHideCart', () => { it('should call quoteItemsComponentService correctly if argument is true', () => { + fixture.detectChanges(); component.onToggleShowOrHideCart(true); expect( quoteItemsComponentService.setQuoteEntriesExpanded @@ -207,6 +209,7 @@ describe('QuoteItemsComponent', () => { }); it('should call quoteItemsComponentService correctly if argument is false', () => { + fixture.detectChanges(); component.onToggleShowOrHideCart(false); expect( quoteItemsComponentService.setQuoteEntriesExpanded @@ -215,6 +218,7 @@ describe('QuoteItemsComponent', () => { }); it('should display CARET_UP per default', () => { + fixture.detectChanges(); CommonQuoteTestUtilsService.expectElementToContainText( expect, htmlElem, @@ -235,6 +239,7 @@ describe('QuoteItemsComponent', () => { }); it('should toggle quote entries on enter', () => { + fixture.detectChanges(); CommonQuoteTestUtilsService.clickToggle(htmlElem, true); fixture.detectChanges(); expect( @@ -254,6 +259,7 @@ describe('QuoteItemsComponent', () => { describe('Accessibility', () => { it("should contain 'div' HTML element with 'role' attribute that indicates the role for this element", () => { + fixture.detectChanges(); const element = CommonQuoteTestUtilsService.getElementByClassNameOrTreeOrder( htmlElem, @@ -271,6 +277,7 @@ describe('QuoteItemsComponent', () => { }); it("should contain 'div' HTML element with 'aria-label' attribute that indicates the text for this element", () => { + fixture.detectChanges(); const element = CommonQuoteTestUtilsService.getElementByClassNameOrTreeOrder( htmlElem, diff --git a/feature-libs/quote/components/links/quote-links.component.spec.ts b/feature-libs/quote/components/links/quote-links.component.spec.ts index b7b9b09bbb0..a4a826e1a28 100644 --- a/feature-libs/quote/components/links/quote-links.component.spec.ts +++ b/feature-libs/quote/components/links/quote-links.component.spec.ts @@ -1,9 +1,5 @@ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { Router, RouterModule, Routes } from '@angular/router'; import { EventService, @@ -33,10 +29,9 @@ import { BehaviorSubject, NEVER, Observable, of, throwError } from 'rxjs'; import { createEmptyQuote } from '../../core/testing/quote-test-utils'; import { CommonQuoteTestUtilsService } from '../testing/common-quote-test-utils.service'; import { QuoteLinksComponent } from './quote-links.component'; -import createSpy = jasmine.createSpy; class MockCartUtilsService implements Partial { - goToNewCart = createSpy(); + goToNewCart = vi.fn(); } const mockRoutes = [{ path: 'cxRoute:quotes', component: {} }] as Routes; @@ -162,7 +157,7 @@ describe('QuoteLinksComponent', () => { }); it('should dispatch QuoteDetailsReloadQueryEvent when component is initialized', () => { - spyOn(eventService, 'dispatch').and.callThrough(); + vi.spyOn(eventService, 'dispatch'); component.ngOnInit(); expect(eventService.dispatch).toHaveBeenCalledWith( {}, @@ -212,7 +207,7 @@ describe('QuoteLinksComponent', () => { }); it('should fire `goToNewCart()` when "New Cart" button was clicked', () => { - spyOn(eventService, 'dispatch').and.callThrough(); + vi.spyOn(eventService, 'dispatch'); const link = CommonQuoteTestUtilsService.getHTMLElement( htmlElem, 'a.link', @@ -226,7 +221,8 @@ describe('QuoteLinksComponent', () => { ); }); - it('should redirect to Quotes list when "Quotes" button was clicked', fakeAsync(() => { + it('should redirect to Quotes list when "Quotes" button was clicked', async () => { + vi.useFakeTimers(); fixture.detectChanges(); const link = CommonQuoteTestUtilsService.getHTMLElement( htmlElem, @@ -234,10 +230,11 @@ describe('QuoteLinksComponent', () => { 1 ); link.click(); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(router.url).toBe('/cxRoute:quotes'); - })); + }); describe('Download proposal document', () => { const vendorQuote: Quote = { @@ -284,11 +281,10 @@ describe('QuoteLinksComponent', () => { }); it('should download the proposal document attached when Download button is clicked', () => { - const spyDownloadAttachment = spyOn( - quoteFacade, - 'downloadAttachment' - ).and.returnValue(of(mockQuoteAttachment())); - const spyDownload = spyOn(fileDownloadService, 'download'); + const spyDownloadAttachment = vi + .spyOn(quoteFacade, 'downloadAttachment') + .mockReturnValue(of(mockQuoteAttachment())); + const spyDownload = vi.spyOn(fileDownloadService, 'download'); mockQuoteDetails$.next(vendorQuote); fixture.detectChanges(); const downloadBtn = CommonQuoteTestUtilsService.getHTMLElement( @@ -308,11 +304,10 @@ describe('QuoteLinksComponent', () => { }); it('should display error message when download fails', () => { - const spyDownloadAttachment = spyOn( - quoteFacade, - 'downloadAttachment' - ).and.returnValue(throwError(() => new Error(errorResponse.message))); - const spyMessage = spyOn(globalMessageService, 'add'); + const spyDownloadAttachment = vi + .spyOn(quoteFacade, 'downloadAttachment') + .mockReturnValue(throwError(() => new Error(errorResponse.message))); + const spyMessage = vi.spyOn(globalMessageService, 'add'); mockQuoteDetails$.next(vendorQuote); fixture.detectChanges(); const downloadBtn = CommonQuoteTestUtilsService.getHTMLElement( @@ -337,7 +332,7 @@ describe('QuoteLinksComponent', () => { const anchorElements = fixture.nativeElement.querySelectorAll('a.cx-action-link'); const orderLink = Array.from(anchorElements).find( - (el: any) => el.innerText.trim() === 'quote.links.order' + (el: any) => (el.textContent || '').trim() === 'quote.links.order' ); expect(orderLink).toBeUndefined(); }); @@ -347,7 +342,7 @@ describe('QuoteLinksComponent', () => { const anchorElements = fixture.nativeElement.querySelectorAll('a.cx-action-link'); const orderLink = Array.from(anchorElements).find( - (el: any) => el.innerText.trim() === 'quote.links.order' + (el: any) => (el.textContent || '').trim() === 'quote.links.order' ); expect(orderLink).not.toBeUndefined(); expect((orderLink as HTMLAnchorElement).href).toContain( diff --git a/feature-libs/quote/components/list/quote-list-component.service.spec.ts b/feature-libs/quote/components/list/quote-list-component.service.spec.ts index ad30aaba28b..0504b9899a6 100644 --- a/feature-libs/quote/components/list/quote-list-component.service.spec.ts +++ b/feature-libs/quote/components/list/quote-list-component.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { I18nTestingModule, PaginationModel, @@ -11,11 +12,10 @@ import { QuoteFacade, QuoteList, } from '@spartacus/quote/root'; -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, firstValueFrom, of } from 'rxjs'; import { take } from 'rxjs/operators'; import { createEmptyQuote } from '../../core/testing/quote-test-utils'; import { QuoteListComponentService } from './quote-list-component.service'; -import createSpy = jasmine.createSpy; const mockCartId = '1234'; const mockPagination: PaginationModel = { @@ -55,8 +55,8 @@ class MockCommerceQuotesFacade implements Partial { getQuotesState(): Observable> { return mockQuoteListState$.asObservable(); } - setSort = createSpy(); - setCurrentPage = createSpy(); + setSort = vi.fn(); + setCurrentPage = vi.fn(); } class MockTranslationService implements Partial { @@ -67,7 +67,7 @@ class MockTranslationService implements Partial { describe('QuoteListComponentService', () => { let classUnderTest: QuoteListComponentService; - let translateSpy: jasmine.Spy; + let translateSpy: ReturnType; beforeEach(() => { TestBed.configureTestingModule({ @@ -87,10 +87,8 @@ describe('QuoteListComponentService', () => { }); beforeEach(() => { - translateSpy = spyOn( - MockTranslationService.prototype, - 'translate' - ).and.callThrough(); + vi.restoreAllMocks(); + translateSpy = vi.spyOn(MockTranslationService.prototype, 'translate'); classUnderTest = TestBed.inject(QuoteListComponentService); }); @@ -99,7 +97,7 @@ describe('QuoteListComponentService', () => { expect(classUnderTest).toBeTruthy(); }); - it('should get translated sort labels', (done) => { + it('should get translated sort labels', async () => { //given const labels: { [key: string]: string } = { byDate: 'sorting.date', @@ -109,28 +107,24 @@ describe('QuoteListComponentService', () => { }; //then - classUnderTest.sortLabels$.subscribe((result) => { - expect(result).toEqual(labels); - expect(translateSpy).toHaveBeenCalledTimes(4); - Object.keys(labels).forEach((key, index) => { - expect(translateSpy.calls.argsFor(index)).toEqual([labels[key]]); - }); - done(); + const result = await firstValueFrom(classUnderTest.sortLabels$); + expect(result).toEqual(labels); + expect(translateSpy).toHaveBeenCalledTimes(4); + Object.keys(labels).forEach((key, index) => { + expect(translateSpy.mock.calls[index]).toEqual([labels[key]]); }); }); //TODO CHHI : remove after fix in OCC - it('should console warning if sorts are received from API', (done) => { + it('should console warning if sorts are received from API', async () => { //given mockQuoteListState$.next(mockListWithSorts); - //const warnSpy = spyOn(console, 'warn'); + //const warnSpy = vi.spyOn(console, 'warn'); //then - classUnderTest.quotesState$.pipe(take(1)).subscribe(() => { - // expect(warnSpy).toHaveBeenCalledTimes(1); - expect(classUnderTest.sortOptions).toEqual(mockSorts); - }); - done(); + await firstValueFrom(classUnderTest.quotesState$); + // expect(warnSpy).toHaveBeenCalledTimes(1); + expect(classUnderTest.sortOptions).toEqual(mockSorts); }); it('should change sort value when setSort', () => { diff --git a/feature-libs/quote/components/list/quote-list.component.spec.ts b/feature-libs/quote/components/list/quote-list.component.spec.ts index b18c9c0548e..9f68c3d772f 100644 --- a/feature-libs/quote/components/list/quote-list.component.spec.ts +++ b/feature-libs/quote/components/list/quote-list.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, EventEmitter, @@ -32,13 +33,12 @@ import { PaginationComponent, SortingComponent, } from '@spartacus/storefront'; -import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { BehaviorSubject, NEVER, Observable, of } from 'rxjs'; import { createEmptyQuote } from '../../core/testing/quote-test-utils'; import { CommonQuoteTestUtilsService } from '../testing/common-quote-test-utils.service'; import { QuoteListComponentService } from './quote-list-component.service'; import { QuoteListComponent } from './quote-list.component'; -import createSpy = jasmine.createSpy; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; const mockCartId = '1234'; const mockPagination: PaginationModel = { @@ -122,8 +122,8 @@ class MockCommerceQuotesListComponentService quotesState$ = mockQuoteListState$.asObservable(); sort = new BehaviorSubject('byCode'); currentPage = new BehaviorSubject(0); - setSorting = createSpy(); - setPage = createSpy(); + setSorting = vi.fn(); + setPage = vi.fn(); } class MockLanguageService { diff --git a/feature-libs/quote/components/request-button/quote-request-button.component.spec.ts b/feature-libs/quote/components/request-button/quote-request-button.component.spec.ts index 79fd680e4f9..b3c45fac957 100644 --- a/feature-libs/quote/components/request-button/quote-request-button.component.spec.ts +++ b/feature-libs/quote/components/request-button/quote-request-button.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { AuthService, MockTranslatePipe, @@ -9,7 +10,6 @@ import { Quote, QuoteFacade } from '@spartacus/quote/root'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { createEmptyQuote } from '../../core/testing/quote-test-utils'; import { QuoteRequestButtonComponent } from './quote-request-button.component'; -import createSpy = jasmine.createSpy; const quoteCode = 'quote1'; const mockCreatedQuote: Quote = { @@ -18,7 +18,7 @@ const mockCreatedQuote: Quote = { code: quoteCode, }; class MockQuoteFacade implements Partial { - createQuote = createSpy().and.returnValue(of(mockCreatedQuote)); + createQuote = vi.fn().mockReturnValue(of(mockCreatedQuote)); } const loggedIn: BehaviorSubject = new BehaviorSubject(true); @@ -31,7 +31,7 @@ describe('QuoteRequestButtonComponent', () => { let fixture: ComponentFixture; let component: QuoteRequestButtonComponent; let quoteFacade: QuoteFacade; - const mockRoutingService = jasmine.createSpyObj('RoutingService', ['go']); + const mockRoutingService = { go: vi.fn() }; beforeEach(async () => { await TestBed.configureTestingModule({ diff --git a/feature-libs/quote/components/summary/actions/quote-summary-actions.component.spec.ts b/feature-libs/quote/components/summary/actions/quote-summary-actions.component.spec.ts index 3142e0365b9..5d9b35b3286 100644 --- a/feature-libs/quote/components/summary/actions/quote-summary-actions.component.spec.ts +++ b/feature-libs/quote/components/summary/actions/quote-summary-actions.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ElementRef, ViewContainerRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; @@ -22,7 +23,7 @@ import { LAUNCH_CALLER, LaunchDialogService, } from '@spartacus/storefront'; -import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, firstValueFrom, of } from 'rxjs'; import { take } from 'rxjs/operators'; import { createEmptyQuote } from '../../../core/testing/quote-test-utils'; import { @@ -32,7 +33,6 @@ import { import { ConfirmationContext } from '../../confirm-dialog/quote-confirm-dialog.model'; import { CommonQuoteTestUtilsService } from '../../testing/common-quote-test-utils.service'; import { QuoteSummaryActionsComponent } from './quote-summary-actions.component'; -import createSpy = jasmine.createSpy; const mockCartId = '1234'; const mockCode = '3333'; @@ -142,7 +142,7 @@ class MockCommerceQuotesFacade implements Partial { return EMPTY; } - requote = createSpy(); + requote = vi.fn(); } class MockTranslationService implements Partial { @@ -232,7 +232,7 @@ describe('QuoteSummaryActionsComponent', () => { intersectionService = TestBed.inject(IntersectionService); mockQuoteDetails$.next(mockQuote); dialogClose$ = new BehaviorSubject(undefined); - spyOn(quoteStorefrontUtilsService, 'changeStyling').and.callThrough(); + vi.spyOn(quoteStorefrontUtilsService, 'changeStyling'); }); it('should create component', () => { @@ -240,15 +240,13 @@ describe('QuoteSummaryActionsComponent', () => { expect(quoteFacade).toBeDefined(); }); - it('should read quote details state', (done) => { - component.quoteDetails$.pipe(take(1)).subscribe((state) => { - expect(state).toEqual(mockQuote); - done(); - }); + it('should read quote details state', async () => { + const state = await firstValueFrom(component.quoteDetails$); + expect(state).toEqual(mockQuote); }); it('should open confirmation dialog when action is SUBMIT', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const quoteForSubmitAction: Quote = { ...mockQuote, allowedActions: [ @@ -281,7 +279,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should open confirmation dialog when action is EDIT and state is BUYER_OFFER', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const quoteInBuyerOfferState: Quote = { ...mockQuote, allowedActions: [ @@ -317,7 +315,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should not open confirmation dialog when action is CANCEL and state is BUYER_DRAFT', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const quoteInBuyerDraftState: Quote = { ...mockQuote, allowedActions: [ @@ -338,7 +336,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should not open confirmation dialog when action is EDIT and state is BUYER_DRAFT and cart is empty', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const quoteInBuyerDraftState: Quote = { ...mockQuote, allowedActions: [ @@ -359,7 +357,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should not open confirmation dialog when action is EDIT and state is BUYER_DRAFT and cart is not empty but is a quote cart', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const quoteInBuyerDraftState: Quote = { ...mockQuote, allowedActions: [ @@ -382,7 +380,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should open confirmation dialog when action is EDIT and state is BUYER_DRAFT and cart is not empty', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const quoteInBuyerDraftState: Quote = { ...mockQuote, allowedActions: [ @@ -419,7 +417,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should open confirmation dialog when action is REQUOTE and state is EXPIRED', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const expiredQuote: Quote = { ...mockQuote, allowedActions: [{ type: QuoteActionType.REQUOTE, isPrimary: true }], @@ -447,7 +445,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should open confirmation dialog when action is REQUOTE and state is CANCELLED and cart has entries', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const cancelledQuote: Quote = { ...mockQuote, allowedActions: [{ type: QuoteActionType.REQUOTE, isPrimary: true }], @@ -477,7 +475,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should not open confirmation dialog when action is REQUOTE and state is CANCELLED and cart has no entries', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const cancelledQuote: Quote = { ...mockQuote, allowedActions: [{ type: QuoteActionType.REQUOTE, isPrimary: true }], @@ -493,7 +491,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should not open confirmation dialog when action is REQUOTE and state is CANCELLED and cart has entries but is a quote-cart', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); const cancelledQuote: Quote = { ...mockQuote, allowedActions: [{ type: QuoteActionType.REQUOTE, isPrimary: true }], @@ -558,7 +556,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should disable submit button if threshold is not met and raise message', () => { - spyOn(globalMessageService, 'add').and.callThrough(); + vi.spyOn(globalMessageService, 'add'); mockQuoteDetails$.next(quoteFailingThreshold); fixture.detectChanges(); @@ -606,7 +604,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should not raise message in case threshold not met and submit action not present', () => { - spyOn(globalMessageService, 'add').and.callThrough(); + vi.spyOn(globalMessageService, 'add'); mockQuoteDetails$.next(cancellableQuote); fixture.detectChanges(); @@ -615,7 +613,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should perform quote action when action is SUBMIT and confirm dialogClose reason is yes', () => { - spyOn(quoteFacade, 'performQuoteAction').and.callThrough(); + vi.spyOn(quoteFacade, 'performQuoteAction'); const newMockQuoteWithSubmitAction: Quote = { ...mockQuote, allowedActions: [ @@ -639,7 +637,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it("should click on 'CANCEL' button", () => { - spyOn(quoteFacade, 'performQuoteAction').and.callThrough(); + vi.spyOn(quoteFacade, 'performQuoteAction'); const newMockQuoteWithSubmitAction: Quote = { ...mockQuote, allowedActions: [ @@ -661,7 +659,7 @@ describe('QuoteSummaryActionsComponent', () => { }); it("should click on 'REQUOTE' button", () => { - spyOn(quoteFacade, 'performQuoteAction').and.callThrough(); + vi.spyOn(quoteFacade, 'performQuoteAction'); fixture.detectChanges(); const requoteButton = CommonQuoteTestUtilsService.getHTMLElement( htmlElem, @@ -770,8 +768,8 @@ describe('QuoteSummaryActionsComponent', () => { describe('handleConfirmationDialogClose', () => { let context: ConfirmationContext; beforeEach(() => { - spyOn(quoteFacade, 'performQuoteAction').and.callThrough(); - spyOn(globalMessageService, 'add').and.callThrough(); + vi.spyOn(quoteFacade, 'performQuoteAction'); + vi.spyOn(globalMessageService, 'add'); context = { quote: mockQuote, title: 'title', @@ -920,8 +918,8 @@ describe('QuoteSummaryActionsComponent', () => { describe('handleScroll', () => { it('should call handleScroll method', () => { - spyOn(quoteStorefrontUtilsService, 'getElement').and.returnValue(slot); - spyOn(quoteStorefrontUtilsService, 'getWindowHeight').and.returnValue( + vi.spyOn(quoteStorefrontUtilsService, 'getElement').mockReturnValue(slot); + vi.spyOn(quoteStorefrontUtilsService, 'getWindowHeight').mockReturnValue( 500 ); component.handleScroll(); @@ -936,12 +934,12 @@ describe('QuoteSummaryActionsComponent', () => { describe('getActionButtonsHeight', () => { it('should return the default height of action buttons', () => { - spyOn(quoteStorefrontUtilsService, 'getHeight').and.returnValue(0); + vi.spyOn(quoteStorefrontUtilsService, 'getHeight').mockReturnValue(0); expect(component['getActionButtonsHeight']()).toBe(226); }); it('should return the actual height of action buttons', () => { - spyOn(quoteStorefrontUtilsService, 'getHeight').and.returnValue(300); + vi.spyOn(quoteStorefrontUtilsService, 'getHeight').mockReturnValue(300); expect(component['getActionButtonsHeight']()).toBe(300); }); }); @@ -975,23 +973,40 @@ describe('QuoteSummaryActionsComponent', () => { describe('Floating action buttons', () => { describe('mobile device', () => { beforeEach(() => { - spyOn(quoteStorefrontUtilsService, 'getElement') - .withArgs('cx-page-slot.CenterRightContent') - .and.returnValue(slot); + vi.spyOn(quoteStorefrontUtilsService, 'getElement').mockImplementation( + (selector) => { + if (selector === 'cx-page-slot.CenterRightContent') { + return slot; + } + return null; + } + ); - spyOn(quoteStorefrontUtilsService, 'getHeight') - .withArgs('cx-quote-summary-actions section') - .and.returnValue(250); + vi.spyOn(quoteStorefrontUtilsService, 'getHeight').mockImplementation( + (selector) => { + if (selector === 'cx-quote-summary-actions section') { + return 250; + } + return 0; + } + ); }); it('should adjust bottom property to zero when there is enough spare viewport', () => { - spyOn(quoteStorefrontUtilsService, 'getDomRectValue') - .withArgs('.BottomHeaderSlot', 'bottom') - .and.returnValue(250); - - spyOn(quoteStorefrontUtilsService, 'getWindowHeight').and.returnValue( - 800 - ); + vi.spyOn( + quoteStorefrontUtilsService, + 'getDomRectValue' + ).mockImplementation((selector, property) => { + if (selector === '.BottomHeaderSlot' && property === 'bottom') { + return 250; + } + return undefined; + }); + + vi.spyOn( + quoteStorefrontUtilsService, + 'getWindowHeight' + ).mockReturnValue(800); component.ngAfterViewInit(); @@ -1003,13 +1018,20 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should adjust bottom property accordingly when there is not enough spare viewport', () => { - spyOn(quoteStorefrontUtilsService, 'getDomRectValue') - .withArgs('.BottomHeaderSlot', 'bottom') - .and.returnValue(378); - - spyOn(quoteStorefrontUtilsService, 'getWindowHeight').and.returnValue( - 500 - ); + vi.spyOn( + quoteStorefrontUtilsService, + 'getDomRectValue' + ).mockImplementation((selector, property) => { + if (selector === '.BottomHeaderSlot' && property === 'bottom') { + return 378; + } + return undefined; + }); + + vi.spyOn( + quoteStorefrontUtilsService, + 'getWindowHeight' + ).mockReturnValue(500); component.ngAfterViewInit(); expect(quoteStorefrontUtilsService.changeStyling).toHaveBeenCalledWith( @@ -1020,7 +1042,9 @@ describe('QuoteSummaryActionsComponent', () => { }); it('should make action buttons sticky when intersecting', () => { - spyOn(intersectionService, 'isIntersecting').and.returnValue(of(true)); + vi.spyOn(intersectionService, 'isIntersecting').mockReturnValue( + of(true) + ); component.ngAfterViewInit(); expect(component.isFixedPosition).toBe(false); diff --git a/feature-libs/quote/components/summary/prices/quote-summary-prices.component.spec.ts b/feature-libs/quote/components/summary/prices/quote-summary-prices.component.spec.ts index 19ac29cd1d3..330473c5d53 100644 --- a/feature-libs/quote/components/summary/prices/quote-summary-prices.component.spec.ts +++ b/feature-libs/quote/components/summary/prices/quote-summary-prices.component.spec.ts @@ -40,7 +40,7 @@ describe('QuoteSummaryPricesComponent', () => { htmlElem = fixture.nativeElement; component = fixture.componentInstance; withPrices(); - fixture.detectChanges(); + // No detectChanges() here — tests that change quote state call it themselves }); function withPrices() { @@ -51,10 +51,12 @@ describe('QuoteSummaryPricesComponent', () => { } it('should create component', () => { + fixture.detectChanges(); expect(component).toBeDefined(); }); it('should display all prices and discounts when present', () => { + fixture.detectChanges(); TestUtil.expectNumberOfElementsPresent( expect, htmlElem, diff --git a/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.service.spec.ts b/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.service.spec.ts index 34aaa138ebd..22b4c611b59 100644 --- a/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.service.spec.ts +++ b/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.service.spec.ts @@ -1,9 +1,10 @@ import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { FormControl, FormGroup } from '@angular/forms'; import { LanguageService, TimeUtils } from '@spartacus/core'; import { Quote, QuoteState } from '@spartacus/quote/root'; -import { Observable, of } from 'rxjs'; +import { Observable, firstValueFrom, of } from 'rxjs'; import { EXPIRATION_DATE_AS_STRING, EXPIRATION_TIME_AS_STRING, @@ -79,48 +80,46 @@ describe('QuoteSummarySellerEditComponentService', () => { }); describe('parseDiscountValue', () => { - it('should parse string', (done) => { - classUnderTest.parseDiscountValue('100.00').subscribe((result) => { - expect(result).toBe(100); - done(); - }); + it('should parse string', async () => { + const result = await firstValueFrom( + classUnderTest.parseDiscountValue('100.00') + ); + expect(result).toBe(100); }); - it('should consider locale specific decimal separator', (done) => { - classUnderTest.parseDiscountValue('100.77').subscribe((result) => { - expect(result).toBe(100.77); - done(); - }); + it('should consider locale specific decimal separator', async () => { + const result = await firstValueFrom( + classUnderTest.parseDiscountValue('100.77') + ); + expect(result).toBe(100.77); }); - it('should ignore locale specific grouping separator', (done) => { - classUnderTest.parseDiscountValue('1,000.77').subscribe((result) => { - expect(result).toBe(1000.77); - done(); - }); + it('should ignore locale specific grouping separator', async () => { + const result = await firstValueFrom( + classUnderTest.parseDiscountValue('1,000.77') + ); + expect(result).toBe(1000.77); }); - it('should handle undefined discount value by returning 0', (done) => { - classUnderTest.parseDiscountValue(undefined).subscribe((result) => { - expect(result).toBe(0); - done(); - }); + it('should handle undefined discount value by returning 0', async () => { + const result = await firstValueFrom( + classUnderTest.parseDiscountValue(undefined) + ); + expect(result).toBe(0); }); - it('should handle null discount value by returning 0', (done) => { - classUnderTest.parseDiscountValue(null).subscribe((result) => { - expect(result).toBe(0); - done(); - }); + it('should handle null discount value by returning 0', async () => { + const result = await firstValueFrom( + classUnderTest.parseDiscountValue(null) + ); + expect(result).toBe(0); }); }); describe('getFormatter', () => { - it('should return a formatter for percentage display ', (done) => { - classUnderTest.getFormatter().subscribe((result) => { - expect(result.format(0)).toBe('0%'); - done(); - }); + it('should return a formatter for percentage display ', async () => { + const result = await firstValueFrom(classUnderTest.getFormatter()); + expect(result.format(0)).toBe('0%'); }); }); diff --git a/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.spec.ts b/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.spec.ts index 0eda1794e51..0c29d12a0f8 100644 --- a/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.spec.ts +++ b/feature-libs/quote/components/summary/seller-edit/quote-summary-seller-edit.component.spec.ts @@ -1,9 +1,5 @@ -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { I18nTestingModule, Price, TranslatePipe } from '@spartacus/core'; import { @@ -39,7 +35,6 @@ import { import { QuoteUIConfig } from '../../config'; import { QuoteSummarySellerEditComponent } from './quote-summary-seller-edit.component'; import { QuoteSummarySellerEditComponentService } from './quote-summary-seller-edit.component.service'; -import createSpy = jasmine.createSpy; const mockCartId = '1234'; const threshold = 20; @@ -74,8 +69,8 @@ class MockCommerceQuotesFacade implements Partial { getQuoteDetails(): Observable { return mockQuoteDetails$.asObservable(); } - addDiscount = createSpy(); - editQuote = createSpy(); + addDiscount = vi.fn(); + editQuote = vi.fn(); } let quoteIsEditable = true; class MockQuoteHeaderSellerEditComponentService { @@ -189,7 +184,7 @@ describe('QuoteSummarySellerEditComponent', () => { }); it('should unsubscribe subscription on ngOnDestroy', () => { - const spyUnsubscribe = spyOn(Subscription.prototype, 'unsubscribe'); + const spyUnsubscribe = vi.spyOn(Subscription.prototype, 'unsubscribe'); component.ngOnDestroy(); expect(spyUnsubscribe).toHaveBeenCalled(); }); @@ -292,7 +287,8 @@ describe('QuoteSummarySellerEditComponent', () => { }); describe('onSetDate', () => { - it('should call corresponding facade method after default debounce time', fakeAsync(() => { + it('should call corresponding facade method after default debounce time', async () => { + vi.useFakeTimers(); const expectedQuoteMetaData: QuoteMetadata = { expirationTime: EXPIRATION_TIME_AS_STRING, }; @@ -300,28 +296,31 @@ describe('QuoteSummarySellerEditComponent', () => { component.ngOnInit(); component.onSetDate(QUOTE_CODE); expect(quoteFacade.editQuote).not.toHaveBeenCalled(); - tick(DEFAULT_DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEFAULT_DEBOUNCE_TIME); + vi.useRealTimers(); expect(quoteFacade.editQuote).toHaveBeenCalledWith( QUOTE_CODE, expectedQuoteMetaData ); - })); + }); - it('should call corresponding facade method after configured debounce time', fakeAsync(() => { + it('should call corresponding facade method after configured debounce time', async () => { + vi.useFakeTimers(); const expectedQuoteMetaData: QuoteMetadata = { expirationTime: EXPIRATION_TIME_AS_STRING, }; component.ngOnInit(); component.onSetDate('INVALID'); - tick(DEFAULT_DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEFAULT_DEBOUNCE_TIME); component.onSetDate(QUOTE_CODE); expect(quoteFacade.editQuote).not.toHaveBeenCalled(); - tick(DEBOUNCE_TIME); + await vi.advanceTimersByTimeAsync(DEBOUNCE_TIME); + vi.useRealTimers(); expect(quoteFacade.editQuote).toHaveBeenCalledWith( QUOTE_CODE, expectedQuoteMetaData ); - })); + }); }); describe('mustDisplayValidationMessage', () => { diff --git a/feature-libs/quote/core/connectors/quote.connector.spec.ts b/feature-libs/quote/core/connectors/quote.connector.spec.ts index 6c6653ecb71..43b3adba1cb 100644 --- a/feature-libs/quote/core/connectors/quote.connector.spec.ts +++ b/feature-libs/quote/core/connectors/quote.connector.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { PaginationModel } from '@spartacus/core'; import { of } from 'rxjs'; @@ -12,8 +13,6 @@ import { import { QuoteAdapter } from './quote.adapter'; import { QuoteConnector } from './quote.connector'; -import createSpy = jasmine.createSpy; - const userId = 'user1'; const cartId = 'cart1'; const quoteCode = 'quote1'; @@ -30,53 +29,64 @@ const comment = { }; class MockCommerceQuotesAdapter implements Partial { - getQuotes = createSpy('CommerceQuotesAdapter.getQuotes').and.callFake( - (userId: string, pagination: PaginationModel) => + getQuotes = vi + .fn() + .mockImplementation((userId: string, pagination: PaginationModel) => of(`getQuotes-${userId}-${pagination}`) - ); - createQuote = createSpy('CommerceQuotesAdapter.createQuote').and.callFake( - (userId: string, quoteStarter: QuoteStarter) => + ); + createQuote = vi + .fn() + .mockImplementation((userId: string, quoteStarter: QuoteStarter) => of(`createQuote-${userId}-${quoteStarter}`) - ); - getQuote = createSpy('CommerceQuotesAdapter.getQuote').and.callFake( - (userId: string, quoteCode: string) => of(`getQuote-${userId}-${quoteCode}`) - ); - editQuote = createSpy('CommerceQuotesAdapter.editQuote').and.callFake( - (userId: string, quoteCode: string, quoteMetadata: QuoteMetadata) => - of(`editQuote-${userId}-${quoteCode}-${quoteMetadata}`) - ); - performQuoteAction = createSpy( - 'CommerceQuotesAdapter.performQuoteAction' - ).and.callFake( - (userId: string, quoteCode: string, quoteAction: QuoteActionType) => - of(`performQuoteAction-${userId}-${quoteCode}-${quoteAction}`) - ); - addComment = createSpy('CommerceQuotesAdapter.addComment').and.callFake( - (userId: string, quoteCode: string, quoteComment: Comment) => - of(`addComment-${userId}-${quoteCode}-${quoteComment}`) - ); - addDiscount = createSpy('CommerceQuotesAdapter.addDiscount').and.callFake( - (userId: string, quoteCode: string, discount: QuoteDiscount) => - of(`addDiscount-${userId}-${quoteCode}-${discount}`) - ); - addQuoteEntryComment = createSpy( - 'CommerceQuotesAdapter.addQuoteEntryComment' - ).and.callFake( - ( - userId: string, - quoteCode: string, - entryNumber: string, - comment: Comment - ) => - of( - `addQuoteEntryComment-${userId}-${quoteCode}-${entryNumber}-${comment}` - ) - ); - downloadAttachment = createSpy( - 'CommerceQuotesAdapter.downloadAttachment' - ).and.callFake((userId: string, quoteCode: string, attachmentId: string) => - of(`downloadAttachment-${userId}-${quoteCode}-${attachmentId}`) - ); + ); + getQuote = vi + .fn() + .mockImplementation((userId: string, quoteCode: string) => + of(`getQuote-${userId}-${quoteCode}`) + ); + editQuote = vi + .fn() + .mockImplementation( + (userId: string, quoteCode: string, quoteMetadata: QuoteMetadata) => + of(`editQuote-${userId}-${quoteCode}-${quoteMetadata}`) + ); + performQuoteAction = vi + .fn('CommerceQuotesAdapter.performQuoteAction') + .mockImplementation( + (userId: string, quoteCode: string, quoteAction: QuoteActionType) => + of(`performQuoteAction-${userId}-${quoteCode}-${quoteAction}`) + ); + addComment = vi + .fn() + .mockImplementation( + (userId: string, quoteCode: string, quoteComment: Comment) => + of(`addComment-${userId}-${quoteCode}-${quoteComment}`) + ); + addDiscount = vi + .fn() + .mockImplementation( + (userId: string, quoteCode: string, discount: QuoteDiscount) => + of(`addDiscount-${userId}-${quoteCode}-${discount}`) + ); + addQuoteEntryComment = vi + .fn('CommerceQuotesAdapter.addQuoteEntryComment') + .mockImplementation( + ( + userId: string, + quoteCode: string, + entryNumber: string, + comment: Comment + ) => + of( + `addQuoteEntryComment-${userId}-${quoteCode}-${entryNumber}-${comment}` + ) + ); + downloadAttachment = vi + .fn('CommerceQuotesAdapter.downloadAttachment') + .mockImplementation( + (userId: string, quoteCode: string, attachmentId: string) => + of(`downloadAttachment-${userId}-${quoteCode}-${attachmentId}`) + ); } describe('QuoteConnector', () => { diff --git a/feature-libs/quote/core/event/quote-cart-event.listener.spec.ts b/feature-libs/quote/core/event/quote-cart-event.listener.spec.ts index da5b79e0e3c..be786be7cfc 100644 --- a/feature-libs/quote/core/event/quote-cart-event.listener.spec.ts +++ b/feature-libs/quote/core/event/quote-cart-event.listener.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { CartAddEntryFailEvent, @@ -9,7 +10,6 @@ import { EventService } from '@spartacus/core'; import { NEVER, Observable, Subscription, of } from 'rxjs'; import { QuoteCartEventListener } from './quote-cart-event.listener'; import { QuoteDetailsReloadQueryEvent } from './quote.events'; -import createSpy = jasmine.createSpy; const cartRemoveEntrySuccessEvent = new CartRemoveEntrySuccessEvent(); cartRemoveEntrySuccessEvent.entry = {}; @@ -42,7 +42,7 @@ class MockEventService implements Partial { return addEntryFail ? of(cartAddEntryFailEvent) : NEVER; } } - dispatch = createSpy(); + dispatch = vi.fn(); } describe('QuoteCartEventListener', () => { @@ -102,7 +102,7 @@ describe('QuoteCartEventListener', () => { }); it('should unsubscribe on ngOnDestroy', () => { - const spyUnsubscribe = spyOn(Subscription.prototype, 'unsubscribe'); + const spyUnsubscribe = vi.spyOn(Subscription.prototype, 'unsubscribe'); classUnderTest.ngOnDestroy(); expect(spyUnsubscribe).toHaveBeenCalled(); }); diff --git a/feature-libs/quote/core/facade/quote.service.spec.ts b/feature-libs/quote/core/facade/quote.service.spec.ts index 3401082b6da..ec3eba2ca07 100644 --- a/feature-libs/quote/core/facade/quote.service.spec.ts +++ b/feature-libs/quote/core/facade/quote.service.spec.ts @@ -1,5 +1,6 @@ import { inject, TestBed } from '@angular/core/testing'; import { Params } from '@angular/router'; +import { vi } from 'vitest'; import { ActiveCartFacade, Cart, @@ -30,7 +31,14 @@ import { } from '@spartacus/quote/root'; import { ViewConfig } from '@spartacus/storefront'; import { cold } from 'jasmine-marbles'; -import { BehaviorSubject, EMPTY, Observable, of, throwError } from 'rxjs'; +import { + BehaviorSubject, + EMPTY, + Observable, + firstValueFrom, + of, + throwError, +} from 'rxjs'; import { switchMap, take } from 'rxjs/operators'; import { QuoteConnector } from '../connectors'; import { QuoteDetailsReloadQueryEvent } from '../event/quote.events'; @@ -39,7 +47,6 @@ import { QuoteCartService } from '../services/quote-cart.service'; import { QuoteStorefrontUtilsService } from '../services/quote-storefront-utils.service'; import { createEmptyQuote, QUOTE_CODE } from '../testing/quote-test-utils'; import { QuoteService } from './quote.service'; -import createSpy = jasmine.createSpy; const userId = OCC_USER_ID_CURRENT; const cartId = '1234'; @@ -103,16 +110,16 @@ class MockRoutingService implements Partial { getRouterState() { return mockRouterState$.asObservable() as Observable; } - go = createSpy(); + go = vi.fn(); } class MockUserIdService implements Partial { - takeUserId = createSpy().and.returnValue(of(userId)); + takeUserId = vi.fn().mockReturnValue(of(userId)); } class MockEventService implements Partial { - get = createSpy().and.returnValue(of()); - dispatch = createSpy(); + get = vi.fn().mockReturnValue(of()); + dispatch = vi.fn(); } let isLoggedIn: boolean; @@ -125,9 +132,9 @@ class MockAuthService implements Partial { let isQuoteCartActive: any; let quoteId: any; class MockQuoteCartService { - setQuoteCartActive = createSpy(); - setQuoteId = createSpy(); - setCheckoutAllowed = createSpy(); + setQuoteCartActive = vi.fn(); + setQuoteId = vi.fn(); + setCheckoutAllowed = vi.fn(); isQuoteCartActive() { return of(isQuoteCartActive); } @@ -140,39 +147,39 @@ class MockViewConfig implements ViewConfig { } class MockQuoteConnector implements Partial { - getQuotes = createSpy().and.returnValue(of(quoteList)); - getQuote = createSpy().and.returnValue(of(quote)); - createQuote = createSpy().and.returnValue(of(quote)); - editQuote = createSpy().and.returnValue(of(EMPTY)); - addComment = createSpy().and.returnValue(of(EMPTY)); - addQuoteEntryComment = createSpy().and.returnValue(of(EMPTY)); - performQuoteAction = createSpy().and.returnValue(of(EMPTY)); - addDiscount = createSpy().and.returnValue(of(EMPTY)); - downloadAttachment = createSpy().and.returnValue(of(mockQuoteAttachment())); + getQuotes = vi.fn().mockReturnValue(of(quoteList)); + getQuote = vi.fn().mockReturnValue(of(quote)); + createQuote = vi.fn().mockReturnValue(of(quote)); + editQuote = vi.fn().mockReturnValue(of(EMPTY)); + addComment = vi.fn().mockReturnValue(of(EMPTY)); + addQuoteEntryComment = vi.fn().mockReturnValue(of(EMPTY)); + performQuoteAction = vi.fn().mockReturnValue(of(EMPTY)); + addDiscount = vi.fn().mockReturnValue(of(EMPTY)); + downloadAttachment = vi.fn().mockReturnValue(of(mockQuoteAttachment())); } class MockActiveCartService implements Partial { - reloadActiveCart = createSpy().and.stub(); - takeActiveCartId = createSpy().and.returnValue(of(cartId)); - getActive = createSpy().and.returnValue(of(cart)); + reloadActiveCart = vi.fn().mockImplementation(() => {}); + takeActiveCartId = vi.fn().mockReturnValue(of(cartId)); + getActive = vi.fn().mockReturnValue(of(cart)); } class MockMultiCartFacade implements Partial { - loadCart = createSpy(); - createCart = createSpy().and.returnValue(of({})); + loadCart = vi.fn(); + createCart = vi.fn().mockReturnValue(of({})); } class MockCartUtilsService implements Partial { - handleCartAndGoToQuoteList = createSpy(); + handleCartAndGoToQuoteList = vi.fn(); } class MockGlobalMessageService implements Partial { - remove = createSpy().and.stub(); - add = createSpy().and.stub(); + remove = vi.fn().mockImplementation(() => {}); + add = vi.fn().mockImplementation(() => {}); } class MockSavedCartFacade implements Partial { - editSavedCart = createSpy(); + editSavedCart = vi.fn(); } describe('QuoteService', () => { @@ -191,7 +198,7 @@ describe('QuoteService', () => { beforeEach(() => { let mockedQuoteStorefrontUtilsService = { - getElement: createSpy(), + getElement: vi.fn(), }; TestBed.configureTestingModule({ @@ -246,16 +253,15 @@ describe('QuoteService', () => { expect(activeCartFacade.getActive).toHaveBeenCalled(); } - function checkNoActionPerforming( - quoteActionResult: Observable, - done: any + async function checkNoActionPerforming( + quoteActionResult: Observable ) { - quoteActionResult - .pipe(switchMap(() => classUnderTest['isActionPerforming$'])) - .subscribe((isPerforming) => { - expect(isPerforming).toBe(false); - done(); - }); + const isPerforming = await firstValueFrom( + quoteActionResult.pipe( + switchMap(() => classUnderTest['isActionPerforming$']) + ) + ); + expect(isPerforming).toBe(false); } it('should inject CommerceQuotesService', inject( @@ -316,26 +322,23 @@ describe('QuoteService', () => { }); describe('getQuotesState - reactive request triggering', () => { - it('should fire exactly one HTTP request on initial page load', (done) => { + it('should fire exactly one HTTP request on initial page load', async () => { const currentPage$ = new BehaviorSubject(0); const sort$ = new BehaviorSubject('byCode'); - classUnderTest - .getQuotesState({ currentPage$, sort$ }) - .pipe(take(1)) - .subscribe((state) => { - expect(quoteConnector.getQuotes).toHaveBeenCalledTimes(1); - expect(quoteConnector.getQuotes).toHaveBeenCalledWith(userId, { - currentPage: 0, - sort: 'byCode', - pageSize: pagination.pageSize, - }); - expect(state.data).toEqual(quoteList); - done(); - }); + const state = await firstValueFrom( + classUnderTest.getQuotesState({ currentPage$, sort$ }) + ); + expect(quoteConnector.getQuotes).toHaveBeenCalledTimes(1); + expect(quoteConnector.getQuotes).toHaveBeenCalledWith(userId, { + currentPage: 0, + sort: 'byCode', + pageSize: pagination.pageSize, + }); + expect(state.data).toEqual(quoteList); }); - it('should trigger a new request when currentPage changes', (done) => { + it('should trigger a new request when currentPage changes', async () => { const currentPage$ = new BehaviorSubject(0); const sort$ = new BehaviorSubject('byCode'); @@ -344,29 +347,33 @@ describe('QuoteService', () => { sort$, }); - // Subscribe and wait for initial emission, then change page - quotesState$.pipe(take(1)).subscribe(() => { - // Reset call count after the initial request - (quoteConnector.getQuotes as jasmine.Spy).calls.reset(); - - // Change the current page - currentPage$.next(1); - - // Now subscribe again to get the result of the page change - quotesState$.pipe(take(1)).subscribe((state) => { - expect(quoteConnector.getQuotes).toHaveBeenCalledTimes(1); - expect(quoteConnector.getQuotes).toHaveBeenCalledWith(userId, { - currentPage: 1, - sort: 'byCode', - pageSize: pagination.pageSize, - }); - expect(state.data).toEqual(quoteList); - done(); - }); + // Keep subscription open to stay reactive + const emissions: any[] = []; + const sub = quotesState$.subscribe((s) => emissions.push(s)); + + // Wait for initial emission + await firstValueFrom(quotesState$); + // Reset call count after the initial request + (quoteConnector.getQuotes as any).mockClear(); + + // Change the current page — triggers new request + currentPage$.next(1); + + // Allow async work to settle + await new Promise((r) => setTimeout(r, 0)); + sub.unsubscribe(); + + expect(quoteConnector.getQuotes).toHaveBeenCalledTimes(1); + expect(quoteConnector.getQuotes).toHaveBeenCalledWith(userId, { + currentPage: 1, + sort: 'byCode', + pageSize: pagination.pageSize, }); + const lastEmission = emissions[emissions.length - 1]; + expect(lastEmission.data).toEqual(quoteList); }); - it('should trigger a new request when sort changes', (done) => { + it('should trigger a new request when sort changes', async () => { const currentPage$ = new BehaviorSubject(0); const sort$ = new BehaviorSubject('byCode'); @@ -375,26 +382,30 @@ describe('QuoteService', () => { sort$, }); - // Subscribe and wait for initial emission, then change sort - quotesState$.pipe(take(1)).subscribe(() => { - // Reset call count after the initial request - (quoteConnector.getQuotes as jasmine.Spy).calls.reset(); - - // Change the sort - sort$.next('byDate'); - - // Now subscribe again to get the result of the sort change - quotesState$.pipe(take(1)).subscribe((state) => { - expect(quoteConnector.getQuotes).toHaveBeenCalledTimes(1); - expect(quoteConnector.getQuotes).toHaveBeenCalledWith(userId, { - currentPage: 0, - sort: 'byDate', - pageSize: pagination.pageSize, - }); - expect(state.data).toEqual(quoteList); - done(); - }); + // Keep subscription open to stay reactive + const emissions: any[] = []; + const sub = quotesState$.subscribe((s) => emissions.push(s)); + + // Wait for initial emission + await firstValueFrom(quotesState$); + // Reset call count after the initial request + (quoteConnector.getQuotes as any).mockClear(); + + // Change the sort — triggers new request + sort$.next('byDate'); + + // Allow async work to settle + await new Promise((r) => setTimeout(r, 0)); + sub.unsubscribe(); + + expect(quoteConnector.getQuotes).toHaveBeenCalledTimes(1); + expect(quoteConnector.getQuotes).toHaveBeenCalledWith(userId, { + currentPage: 0, + sort: 'byDate', + pageSize: pagination.pageSize, }); + const lastEmission = emissions[emissions.length - 1]; + expect(lastEmission.data).toEqual(quoteList); }); }); @@ -432,17 +443,12 @@ describe('QuoteService', () => { expect(quoteConnector.getQuote).toHaveBeenCalledTimes(0); }); - it('should wait until active cart has been loaded', (done) => { + it('should wait until active cart has been loaded', async () => { isQuoteCartActive = true; quoteId = quote.code; - classUnderTest - .getQuoteDetails() - .pipe(take(1)) - .subscribe((details) => { - expect(activeCartFacade.getActive).toHaveBeenCalled(); - expect(details).toEqual(quote); - done(); - }); + const details = await firstValueFrom(classUnderTest.getQuoteDetails()); + expect(activeCartFacade.getActive).toHaveBeenCalled(); + expect(details).toEqual(quote); }); it('should call connector once if isStable emits twice', () => { @@ -533,199 +539,166 @@ describe('QuoteService', () => { }); describe('performQuoteAction', () => { - it('should call respective connector method', (done) => { - classUnderTest - .performQuoteAction(quote, quoteAction.type) - .subscribe(() => { - expect(quoteConnector.performQuoteAction).toHaveBeenCalledWith( - userId, - quote.code, - quoteAction.type - ); - done(); - }); + it('should call respective connector method', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, quoteAction.type) + ); + expect(quoteConnector.performQuoteAction).toHaveBeenCalledWith( + userId, + quote.code, + quoteAction.type + ); }); - it('should raise re-load event', (done) => { - classUnderTest - .performQuoteAction(quote, quoteAction.type) - .subscribe(() => { - expect(eventService.dispatch).toHaveBeenCalledWith( - {}, - QuoteDetailsReloadQueryEvent - ); - done(); - }); + it('should raise re-load event', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, quoteAction.type) + ); + expect(eventService.dispatch).toHaveBeenCalledWith( + {}, + QuoteDetailsReloadQueryEvent + ); }); - it('should raise re-load event, even if action fails', (done) => { - quoteConnector.performQuoteAction = createSpy().and.returnValue( - throwError({}) + it('should raise re-load event, even if action fails', async () => { + quoteConnector.performQuoteAction = vi + .fn() + .mockReturnValue(throwError({})); + await firstValueFrom( + classUnderTest.performQuoteAction(quote, quoteAction.type) + ).catch(() => {}); + expect(eventService.dispatch).toHaveBeenCalledWith( + {}, + QuoteDetailsReloadQueryEvent ); - classUnderTest.performQuoteAction(quote, quoteAction.type).subscribe({ - error: () => { - expect(eventService.dispatch).toHaveBeenCalledWith( - {}, - QuoteDetailsReloadQueryEvent - ); - done(); - }, - }); }); describe('on submit', () => { - it('should create new cart and navigate to quote list, but not reload', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.SUBMIT) - .subscribe(() => { - expect( - cartUtilsService.handleCartAndGoToQuoteList - ).toHaveBeenCalled(); - expect(eventService.dispatch).not.toHaveBeenCalledWith( - {}, - QuoteDetailsReloadQueryEvent - ); - done(); - }); + it('should create new cart and navigate to quote list, but not reload', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.SUBMIT) + ); + expect(cartUtilsService.handleCartAndGoToQuoteList).toHaveBeenCalled(); + expect(eventService.dispatch).not.toHaveBeenCalledWith( + {}, + QuoteDetailsReloadQueryEvent + ); }); - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming( - classUnderTest.performQuoteAction(quote, QuoteActionType.SUBMIT), - done + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming( + classUnderTest.performQuoteAction(quote, QuoteActionType.SUBMIT) ); }); }); describe('on cancel', () => { - it('should create new cart and navigate to quote list', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.CANCEL) - .subscribe(() => { - expect( - cartUtilsService.handleCartAndGoToQuoteList - ).toHaveBeenCalled(); - done(); - }); + it('should create new cart and navigate to quote list', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.CANCEL) + ); + expect(cartUtilsService.handleCartAndGoToQuoteList).toHaveBeenCalled(); }); - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming( - classUnderTest.performQuoteAction(quote, QuoteActionType.CANCEL), - done + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming( + classUnderTest.performQuoteAction(quote, QuoteActionType.CANCEL) ); }); }); describe('on edit', () => { - it('should load quote cart', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.EDIT) - .subscribe(() => { - checkQuoteCartFacadeCalls(); - done(); - }); + it('should load quote cart', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.EDIT) + ); + checkQuoteCartFacadeCalls(); }); - it('should trigger a quote refresh', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.EDIT) - .subscribe(() => { - expect(eventService.dispatch).toHaveBeenCalledWith( - {}, - QuoteDetailsReloadQueryEvent - ); - done(); - }); + it('should trigger a quote refresh', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.EDIT) + ); + expect(eventService.dispatch).toHaveBeenCalledWith( + {}, + QuoteDetailsReloadQueryEvent + ); }); - it('should trigger quote re-read in case quote does not carry a cart id', (done) => { - classUnderTest - .performQuoteAction(quoteWithoutCartId, QuoteActionType.EDIT) - .subscribe(() => { - expect(quoteConnector.getQuote).toHaveBeenCalledWith( - userId, - quote.code - ); - done(); - }); + it('should trigger quote re-read in case quote does not carry a cart id', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction( + quoteWithoutCartId, + QuoteActionType.EDIT + ) + ); + expect(quoteConnector.getQuote).toHaveBeenCalledWith( + userId, + quote.code + ); }); - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming( - classUnderTest.performQuoteAction(quote, QuoteActionType.EDIT), - done + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming( + classUnderTest.performQuoteAction(quote, QuoteActionType.EDIT) ); }); }); describe('on checkout', () => { - it('should load cart on checkout and signal that checkout is allowed', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.CHECKOUT) - .subscribe(() => { - checkQuoteCartFacadeCalls(); - expect(quoteCartService.setCheckoutAllowed).toHaveBeenCalledWith( - true - ); - done(); - }); + it('should load cart on checkout and signal that checkout is allowed', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.CHECKOUT) + ); + checkQuoteCartFacadeCalls(); + expect(quoteCartService.setCheckoutAllowed).toHaveBeenCalledWith(true); }); - it('should navigate to checkout', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.CHECKOUT) - .subscribe(() => { - expect(routingService.go).toHaveBeenCalledWith({ - cxRoute: 'checkout', - }); - done(); - }); + it('should navigate to checkout', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.CHECKOUT) + ); + expect(routingService.go).toHaveBeenCalledWith({ + cxRoute: 'checkout', + }); }); - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming( - classUnderTest.performQuoteAction(quote, QuoteActionType.CHECKOUT), - done + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming( + classUnderTest.performQuoteAction(quote, QuoteActionType.CHECKOUT) ); }); }); describe('on reject', () => { - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming( - classUnderTest.performQuoteAction(quote, QuoteActionType.REJECT), - done + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming( + classUnderTest.performQuoteAction(quote, QuoteActionType.REJECT) ); }); - it('trigger navigation to quotes list', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.REJECT) - .subscribe(() => { - expect(routingService.go).toHaveBeenCalledWith({ - cxRoute: 'quotes', - }); - done(); - }); + it('trigger navigation to quotes list', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.REJECT) + ); + expect(routingService.go).toHaveBeenCalledWith({ + cxRoute: 'quotes', + }); }); }); describe('on approve', () => { - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming( - classUnderTest.performQuoteAction(quote, QuoteActionType.APPROVE), - done + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming( + classUnderTest.performQuoteAction(quote, QuoteActionType.APPROVE) ); }); - it('trigger navigation to quotes list', (done) => { - classUnderTest - .performQuoteAction(quote, QuoteActionType.APPROVE) - .subscribe(() => { - expect(routingService.go).toHaveBeenCalledWith({ - cxRoute: 'quotes', - }); - done(); - }); + it('trigger navigation to quotes list', async () => { + await firstValueFrom( + classUnderTest.performQuoteAction(quote, QuoteActionType.APPROVE) + ); + expect(routingService.go).toHaveBeenCalledWith({ + cxRoute: 'quotes', + }); }); }); }); @@ -773,42 +746,43 @@ describe('QuoteService', () => { }); }); - it('should load quote cart', (done) => { - classUnderTest - .requote(quote.code) - .pipe(take(1)) - .subscribe(() => { - checkQuoteCartFacadeCalls(); - done(); - }); + it('should load quote cart', async () => { + await firstValueFrom(classUnderTest.requote(quote.code)); + checkQuoteCartFacadeCalls(); }); - it('should set loading state to false when action is completed', (done) => { - checkNoActionPerforming(classUnderTest.requote(quote.code), done); + it('should set loading state to false when action is completed', async () => { + await checkNoActionPerforming(classUnderTest.requote(quote.code)); }); }); describe('handleError', () => { it('should ignore unknown errors', () => { + let completed = false; classUnderTest['handleError']({ message: 'some error', details: [], }).subscribe({ - complete: () => fail('should signal error'), + complete: () => { + completed = true; + }, error: (error) => { expect(error).toEqual({ message: 'some error', details: [] }); }, }); + expect(completed).toBe(false); }); it('should handle CommerceQuoteExpirationTimeError', () => { + let errored = false; classUnderTest['handleError']({ details: [{ type: 'CommerceQuoteExpirationTimeError' }], }).subscribe({ error: () => { - fail('should NOT signal error'); + errored = true; }, }); + expect(errored).toBe(false); expect(globalMessageService.add).toHaveBeenCalledWith( { key: 'quote.httpHandlers.expired' }, GlobalMessageType.MSG_TYPE_ERROR @@ -841,20 +815,20 @@ describe('QuoteService', () => { }); }); - it('should download proposal document after calling quoteConnector.downloadAttachment', (done) => { + it('should download proposal document after calling quoteConnector.downloadAttachment', async () => { const vendorQuoteCode = vendorQuote.code; const vendorQuoteAttachmentId = vendorQuote.sapAttachments[0].id; - classUnderTest - .downloadAttachment(vendorQuoteCode, vendorQuoteAttachmentId) - .pipe(take(1)) - .subscribe((response) => { - expect(quoteConnector.downloadAttachment).toHaveBeenCalledWith( - userId, - vendorQuoteCode, - vendorQuoteAttachmentId - ); - expect(response).toEqual(mockQuoteAttachment()); - done(); - }); + const response = await firstValueFrom( + classUnderTest.downloadAttachment( + vendorQuoteCode, + vendorQuoteAttachmentId + ) + ); + expect(quoteConnector.downloadAttachment).toHaveBeenCalledWith( + userId, + vendorQuoteCode, + vendorQuoteAttachmentId + ); + expect(response).toEqual(mockQuoteAttachment()); }); }); diff --git a/feature-libs/quote/core/services/cart-utils.service.spec.ts b/feature-libs/quote/core/services/cart-utils.service.spec.ts index 6dbc1c9955c..c4c2fdfd87c 100644 --- a/feature-libs/quote/core/services/cart-utils.service.spec.ts +++ b/feature-libs/quote/core/services/cart-utils.service.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { Cart, MultiCartFacade } from '@spartacus/cart/base/root'; -import { Observable, of } from 'rxjs'; +import { Observable, firstValueFrom, of } from 'rxjs'; import { UserIdService, RoutingService } from '@spartacus/core'; import { CartUtilsService } from './cart-utils.service'; -import createSpy = jasmine.createSpy; const newCart: Cart = {}; @@ -20,7 +20,7 @@ class MockUserIdService implements Partial { } class MockRoutingService implements Partial { - go = createSpy(); + go = vi.fn(); } describe('CartUtilsService', () => { @@ -42,8 +42,8 @@ describe('CartUtilsService', () => { userIdService = TestBed.inject(UserIdService); routingService = TestBed.inject(RoutingService); multiCartFacade = TestBed.inject(MultiCartFacade); - spyOn(userIdService, 'takeUserId').and.returnValue(of('current')); - spyOn(multiCartFacade, 'createCart').and.callThrough(); + vi.spyOn(userIdService, 'takeUserId').mockReturnValue(of('current')); + vi.spyOn(multiCartFacade, 'createCart'); }); it('should be created', () => { @@ -51,12 +51,10 @@ describe('CartUtilsService', () => { }); describe('createNewCart', () => { - it('should create a new cart ', (done) => { - classUnderTest['createNewCart']().subscribe((cart) => { - expect(cart).toBe(newCart); - expect(userIdService.takeUserId).toHaveBeenCalled(); - done(); - }); + it('should create a new cart ', async () => { + const cart = await firstValueFrom(classUnderTest['createNewCart']()); + expect(cart).toBe(newCart); + expect(userIdService.takeUserId).toHaveBeenCalled(); }); }); diff --git a/feature-libs/quote/core/services/quote-cart.service.spec.ts b/feature-libs/quote/core/services/quote-cart.service.spec.ts index 60c49684bb6..b391a146bd4 100644 --- a/feature-libs/quote/core/services/quote-cart.service.spec.ts +++ b/feature-libs/quote/core/services/quote-cart.service.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { ActiveCartFacade, Cart } from '@spartacus/cart/base/root'; import { of } from 'rxjs'; import { QuoteCartService } from './quote-cart.service'; -import createSpy = jasmine.createSpy; const cartId = '8762'; const quoteAttachedToCart = '6524'; @@ -13,10 +13,10 @@ const cart: Cart = { }; class MockActiveCartFacade implements Partial { - reloadActiveCart = createSpy().and.stub(); - takeActiveCartId = createSpy().and.returnValue(of(cartId)); - requireLoadedCart = createSpy().and.returnValue(of(cart)); - getActive = createSpy().and.returnValue(of(cart)); + reloadActiveCart = vi.fn().mockImplementation(() => {}); + takeActiveCartId = vi.fn().mockReturnValue(of(cartId)); + requireLoadedCart = vi.fn().mockReturnValue(of(cart)); + getActive = vi.fn().mockReturnValue(of(cart)); } describe('QuoteCartService', () => { diff --git a/feature-libs/quote/core/services/quote-storefront-utils.service.spec.ts b/feature-libs/quote/core/services/quote-storefront-utils.service.spec.ts index 698bd7cba92..ba7cc3dad24 100644 --- a/feature-libs/quote/core/services/quote-storefront-utils.service.spec.ts +++ b/feature-libs/quote/core/services/quote-storefront-utils.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { WindowRef } from '@spartacus/core'; @@ -24,6 +25,7 @@ class MockedWindowRef extends WindowRef { `, + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class MockQuoteComponent {} @@ -33,10 +35,17 @@ describe('QuoteStorefrontUtilsService', () => { let htmlElem: HTMLElement; let windowRef: WindowRef; + afterEach(() => { + vi.restoreAllMocks(); + if (htmlElem && htmlElem.parentNode) { + document.body.removeChild(htmlElem); + } + htmlElem = null as any; + }); + beforeEach(() => { TestBed.configureTestingModule({ imports: [MockQuoteComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [{ provide: WindowRef, useClass: MockedWindowRef }], }).compileComponents(); @@ -48,46 +57,40 @@ describe('QuoteStorefrontUtilsService', () => { fixture.detectChanges(); }); - afterEach(() => { - if (htmlElem) { - document.body.removeChild(htmlElem); - } - }); - describe('getElement', () => { it('should not get HTML element if not running in browser', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getElement('elementMock')).toBeUndefined(); }); it('should get HTML element based on query selector when running in browser and element exists', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const theElement = document.createElement('elementMock'); - spyOn(windowRef.document, 'querySelector').and.returnValue(theElement); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(theElement); expect(classUnderTest.getElement('elementMock')).toEqual(theElement); }); it('should get null if element does not exist', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); - spyOn(windowRef.document, 'querySelector').and.returnValue(null); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(null); expect(classUnderTest.getElement('unknownElement')).toEqual(null); }); }); describe('changeStyling', () => { it('should not change styling of HTML element if element does not exist', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const element = document.createElement('notExistingElement'); - spyOn(windowRef.document, 'querySelector').and.returnValue(undefined); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(undefined); classUnderTest.changeStyling('notExistingElement', 'position', 'sticky'); expect(element.style.position).not.toEqual('sticky'); }); it('should change styling of HTML element', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); const theElement = document.createElement('elementMock'); - spyOn(windowRef.document, 'querySelector').and.returnValue(theElement); + vi.spyOn(windowRef.document, 'querySelector').mockReturnValue(theElement); classUnderTest.changeStyling('elementMock', 'position', 'sticky'); expect(theElement.style.position).toEqual('sticky'); }); @@ -109,7 +112,7 @@ describe('QuoteStorefrontUtilsService', () => { label.style.height = '50px'; }); - spyOn(list, 'getBoundingClientRect').and.returnValue( + vi.spyOn(list, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 100, 250, 500) ); }); @@ -143,6 +146,11 @@ describe('QuoteStorefrontUtilsService', () => { list.style.flexDirection = 'column'; mockedWindow.innerWidth = undefined; + // jsdom has no layout engine — clientWidth is always 0; mock it so the viewport check passes + Object.defineProperty(list, 'clientWidth', { + value: 1000, + configurable: true, + }); expect(classUnderTest['isInViewport'](list)).toBe(true); }); @@ -153,6 +161,11 @@ describe('QuoteStorefrontUtilsService', () => { list.style.height = '1000px'; mockedWindow.innerHeight = undefined; + // jsdom has no layout engine — clientHeight is always 0; mock it so the viewport check passes + Object.defineProperty(list, 'clientHeight', { + value: 1000, + configurable: true, + }); expect(classUnderTest['isInViewport'](list)).toBe(true); }); @@ -167,7 +180,7 @@ describe('QuoteStorefrontUtilsService', () => { list.style.height = '50px'; list.style.border = 'thick double #32a1ce;'; - spyOn(list, 'getBoundingClientRect').and.returnValue( + vi.spyOn(list, 'getBoundingClientRect').mockReturnValue( new DOMRect(100, 100, 250, 500) ); }); @@ -184,12 +197,44 @@ describe('QuoteStorefrontUtilsService', () => { it('should return offsetHeight of the element because component is in viewport', () => { mockedWindow.innerWidth = 1000; - + Object.defineProperty(list, 'offsetHeight', { + value: 50, + configurable: true, + }); expect(classUnderTest['getHeight']('cx-quote-list')).toBeGreaterThan(0); }); }); describe('getDomRectValue', () => { + let list: HTMLElement; + + beforeEach(() => { + list = htmlElem.querySelector('cx-quote-list') as HTMLElement; + // jsdom's DOMRect.toJSON() is not implemented; provide a stub that the service can call + vi.spyOn(list, 'getBoundingClientRect').mockReturnValue({ + top: 10, + left: 10, + right: 260, + bottom: 510, + width: 250, + height: 500, + x: 10, + y: 10, + toJSON() { + return { + top: 10, + left: 10, + right: 260, + bottom: 510, + width: 250, + height: 500, + x: 10, + y: 10, + }; + }, + } as DOMRect); + }); + it('should return undefined if no element is found by a selector query', () => { expect( classUnderTest['getDomRectValue']('unknown-query', 'bottom') @@ -211,17 +256,19 @@ describe('QuoteStorefrontUtilsService', () => { describe('getWindowHeight', () => { it('should return zero if not running in browser', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); expect(classUnderTest.getWindowHeight()).toBe(0); }); it('should return zero if nativeWindow is undefined', () => { - spyOn(windowRef, 'isBrowser').and.returnValues(true, false); + vi.spyOn(windowRef, 'isBrowser') + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); expect(classUnderTest.getWindowHeight()).toBe(0); }); it('should return the height of the window', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(true); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(true); expect(classUnderTest.getWindowHeight()).toBeGreaterThan(0); }); }); diff --git a/feature-libs/quote/karma.conf.js b/feature-libs/quote/karma.conf.js deleted file mode 100644 index 4ad0534de9a..00000000000 --- a/feature-libs/quote/karma.conf.js +++ /dev/null @@ -1,42 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - ], - client: { - clearContext: false, // leave Jasmine Spec Runner output visible in browser - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots'], - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/quote'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 75, - functions: 90, - }, - }, - }, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/quote/occ/adapters/occ-quote.adapter.spec.ts b/feature-libs/quote/occ/adapters/occ-quote.adapter.spec.ts index 699b5b33cb9..de12638c438 100644 --- a/feature-libs/quote/occ/adapters/occ-quote.adapter.spec.ts +++ b/feature-libs/quote/occ/adapters/occ-quote.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpRequest, provideHttpClient, @@ -33,6 +34,7 @@ import { QuoteMetadata, QuoteStarter, } from '@spartacus/quote/root'; +import { firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { createEmptyQuote } from '../../core/testing/quote-test-utils'; import { OccQuoteAdapter } from './occ-quote.adapter'; @@ -139,24 +141,20 @@ describe(`OccQuoteAdapter`, () => { converterService = TestBed.inject(ConverterService); occEnpointsService = TestBed.inject(OccEndpointsService); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'pipeableMany').and.callThrough(); - spyOn(converterService, 'convert').and.callThrough(); - spyOn(occEnpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'pipeableMany'); + vi.spyOn(converterService, 'convert'); + vi.spyOn(occEnpointsService, 'buildUrl'); }); afterEach(() => { httpTestingController.verify(); }); - it('getQuotes should return users quotes list', (done) => { - classUnderTest - .getQuotes(userId, pagination) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockQuoteList); - done(); - }); + it('getQuotes should return users quotes list', async () => { + const resultPromise = firstValueFrom( + classUnderTest.getQuotes(userId, pagination) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq( @@ -169,19 +167,18 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockQuoteList); + + const result = await resultPromise; + expect(result).toEqual(mockQuoteList); expect(converterService.pipeable).toHaveBeenCalledWith( QUOTE_LIST_NORMALIZER ); }); - it('createQuote should create quote based on provided cartId', (done) => { - classUnderTest - .createQuote(userId, mockQuoteStarter) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockQuote); - done(); - }); + it('createQuote should create quote based on provided cartId', async () => { + const resultPromise = firstValueFrom( + classUnderTest.createQuote(userId, mockQuoteStarter) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'POST', '') @@ -190,6 +187,9 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockQuote); + + const result = await resultPromise; + expect(result).toEqual(mockQuote); expect(converterService.pipeable).toHaveBeenCalledWith(QUOTE_NORMALIZER); expect(converterService.convert).toHaveBeenCalledWith( mockQuoteStarter, @@ -197,14 +197,10 @@ describe(`OccQuoteAdapter`, () => { ); }); - it('getQuote should return quote details based on provided quoteCode without orderCode', (done) => { - classUnderTest - .getQuote(userId, mockQuote.code) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockQuote); - done(); - }); + it('getQuote should return quote details based on provided quoteCode without orderCode', async () => { + const resultPromise = firstValueFrom( + classUnderTest.getQuote(userId, mockQuote.code) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'GET') @@ -221,20 +217,19 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockQuote); + + const result = await resultPromise; + expect(result).toEqual(mockQuote); expect(converterService.pipeable).toHaveBeenCalledWith(QUOTE_NORMALIZER); }); - it('getQuote should return quote details based on provided quoteCode', (done) => { - spyOnProperty(MockOrderConfig, 'showOrderQuoteLink', 'get').and.returnValue( + it('getQuote should return quote details based on provided quoteCode', async () => { + vi.spyOn(MockOrderConfig, 'showOrderQuoteLink', 'get').mockReturnValue( true ); - classUnderTest - .getQuote(userId, mockQuote.code) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockQuote); - done(); - }); + const resultPromise = firstValueFrom( + classUnderTest.getQuote(userId, mockQuote.code) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'GET') @@ -243,6 +238,9 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockQuote); + + const result = await resultPromise; + expect(result).toEqual(mockQuote); expect(converterService.pipeable).toHaveBeenCalledWith(QUOTE_NORMALIZER); expect(occEnpointsService.buildUrl).toHaveBeenCalledWith('getQuote', { urlParams: { userId, quoteCode: mockQuote.code }, @@ -252,19 +250,10 @@ describe(`OccQuoteAdapter`, () => { }); }); - it('getQuote should call httpErrorHandler on error', (done) => { - classUnderTest - .getQuote(userId, mockQuote.code) - .pipe(take(1)) - .subscribe( - () => { - fail('error expected'); - }, - (error) => { - expect(isErrorNormalized(error)).toBe(true); - done(); - } - ); + it('getQuote should call httpErrorHandler on error', async () => { + const resultPromise = firstValueFrom( + classUnderTest.getQuote(userId, mockQuote.code) + ).catch((error) => ({ error })); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'GET') @@ -273,16 +262,15 @@ describe(`OccQuoteAdapter`, () => { status: 400, statusText: 'Bad request', }); + + const result = (await resultPromise) as any; + expect(isErrorNormalized(result.error)).toBe(true); }); - it('editQuote should editQuote quote', (done) => { - classUnderTest - .editQuote(userId, mockQuote.code, mockQuoteMetadata) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(null); - done(); - }); + it('editQuote should editQuote quote', async () => { + const resultPromise = firstValueFrom( + classUnderTest.editQuote(userId, mockQuote.code, mockQuoteMetadata) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'PATCH') @@ -291,20 +279,19 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(null); + + const result = await resultPromise; + expect(result).toEqual(null); expect(converterService.convert).toHaveBeenCalledWith( mockQuoteMetadata, QUOTE_METADATA_SERIALIZER ); }); - it('performQuoteAction should send action to be performed for quote', (done) => { - classUnderTest - .performQuoteAction(userId, mockQuote.code, mockQuoteAction) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(null); - done(); - }); + it('performQuoteAction should send action to be performed for quote', async () => { + const resultPromise = firstValueFrom( + classUnderTest.performQuoteAction(userId, mockQuote.code, mockQuoteAction) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'POST', `/${mockQuote.code}/action`) @@ -313,20 +300,19 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(null); + + const result = await resultPromise; + expect(result).toEqual(null); expect(converterService.convert).toHaveBeenCalledWith( mockQuoteAction, QUOTE_ACTION_SERIALIZER ); }); - it('addComment should add comment to quote', (done) => { - classUnderTest - .addComment(userId, mockQuote.code, mockQuoteComment) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(null); - done(); - }); + it('addComment should add comment to quote', async () => { + const resultPromise = firstValueFrom( + classUnderTest.addComment(userId, mockQuote.code, mockQuoteComment) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'POST', `/${mockQuote.code}/comments`) @@ -335,20 +321,19 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(null); + + const result = await resultPromise; + expect(result).toEqual(null); expect(converterService.convert).toHaveBeenCalledWith( mockQuoteComment, QUOTE_COMMENT_SERIALIZER ); }); - it('addDiscount should add discount to quote', (done) => { - classUnderTest - .addDiscount(userId, mockQuote.code, mockQuoteDiscount) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(null); - done(); - }); + it('addDiscount should add discount to quote', async () => { + const resultPromise = firstValueFrom( + classUnderTest.addDiscount(userId, mockQuote.code, mockQuoteDiscount) + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq(req, 'POST', `/${mockQuote.code}/discounts`) @@ -357,25 +342,24 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(null); + + const result = await resultPromise; + expect(result).toEqual(null); expect(converterService.convert).toHaveBeenCalledWith( mockQuoteDiscount, QUOTE_DISCOUNT_SERIALIZER ); }); - it('addQuoteEntryComment should add comment to product entry in quote cart', (done) => { - classUnderTest - .addQuoteEntryComment( + it('addQuoteEntryComment should add comment to product entry in quote cart', async () => { + const resultPromise = firstValueFrom( + classUnderTest.addQuoteEntryComment( userId, mockQuote.code, productEntryNumber, mockQuoteEntryComment ) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(null); - done(); - }); + ); const mockReq = httpTestingController.expectOne((req) => isQuoteReq( @@ -388,6 +372,9 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(null); + + const result = await resultPromise; + expect(result).toEqual(null); expect(converterService.convert).toHaveBeenCalledWith( mockQuoteEntryComment, QUOTE_COMMENT_SERIALIZER @@ -395,17 +382,17 @@ describe(`OccQuoteAdapter`, () => { }); describe('downloadAttachment', () => { - it('should download proposal document based on provided quoteCode and attachmentId', (done) => { + it('should download proposal document based on provided quoteCode and attachmentId', async () => { const vendorQuoteCode = vendorQuote.code; const vendorQuoteAttachmentId = vendorQuote.sapAttachments[0].id; - classUnderTest - .downloadAttachment(userId, vendorQuoteCode, vendorQuoteAttachmentId) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockQuoteAttachment()); - done(); - }); + const resultPromise = firstValueFrom( + classUnderTest.downloadAttachment( + userId, + vendorQuoteCode, + vendorQuoteAttachmentId + ) + ); const mockReq = httpTestingController.expectOne( (req) => @@ -417,24 +404,22 @@ describe(`OccQuoteAdapter`, () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('blob'); mockReq.flush(mockQuoteAttachment()); + + const result = await resultPromise; + expect(result).toEqual(mockQuoteAttachment()); }); - it('should call httpErrorHandler on error', (done) => { + it('should call httpErrorHandler on error', async () => { const vendorQuoteCode = vendorQuote.code; const vendorQuoteAttachmentId = vendorQuote.sapAttachments[0].id; - classUnderTest - .downloadAttachment(userId, vendorQuoteCode, vendorQuoteAttachmentId) - .pipe(take(1)) - .subscribe({ - next: () => { - fail('error expected'); - }, - error: (error) => { - expect(isErrorNormalized(error)).toBe(true); - done(); - }, - }); + const resultPromise = firstValueFrom( + classUnderTest.downloadAttachment( + userId, + vendorQuoteCode, + vendorQuoteAttachmentId + ) + ).catch((error) => ({ error })); const mockReq = httpTestingController.expectOne( (req) => @@ -446,6 +431,9 @@ describe(`OccQuoteAdapter`, () => { status: 400, statusText: 'Bad request', }); + + const result = (await resultPromise) as any; + expect(isErrorNormalized(result.error)).toBe(true); }); }); diff --git a/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts b/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts index 1ac093fb665..217fa49264f 100644 --- a/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts +++ b/feature-libs/quote/occ/converters/occ-quote-entry-normalizer.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { ConverterService, PRODUCT_NORMALIZER } from '@spartacus/core'; import { QuoteState } from '@spartacus/quote/root'; @@ -34,7 +35,7 @@ describe('OccQuoteEntryNormalizer', () => { classUnderTest = TestBed.inject(OccQuoteEntryNormalizer); converterService = TestBed.inject(ConverterService); - spyOn(converterService, 'convert').and.callThrough(); + vi.spyOn(converterService, 'convert'); }); it('should be created', () => { diff --git a/feature-libs/quote/project.json b/feature-libs/quote/project.json index 398e2b6db1b..8cca975475e 100644 --- a/feature-libs/quote/project.json +++ b/feature-libs/quote/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/quote/test.ts", - "tsConfig": "feature-libs/quote/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/quote/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/quote/root/http-interceptors/quote-bad-request.handler.spec.ts b/feature-libs/quote/root/http-interceptors/quote-bad-request.handler.spec.ts index f0312d186cd..ed70da445ea 100644 --- a/feature-libs/quote/root/http-interceptors/quote-bad-request.handler.spec.ts +++ b/feature-libs/quote/root/http-interceptors/quote-bad-request.handler.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpRequest } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { @@ -122,7 +123,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should handle threshold error', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockQuoteUnderThresholdResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -134,7 +135,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should handle cart validation error', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockCartValidationResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -146,7 +147,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should handle quote cart access error issues', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockQuoteAccessErrorResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -158,7 +159,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should handle quote discount error', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockQuoteDiscountResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -170,7 +171,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should handle expiration date error', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockQuoteExpirationDateResponse); expect(globalMessageService.add).toHaveBeenCalledWith( @@ -182,7 +183,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should raise no message for IllegalArgumentErrors that are not related to quote discounts', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockIllegalArgumentResponse); @@ -190,7 +191,7 @@ describe('QuoteBadRequestHandler', () => { }); it('should be able to deal with an empty error response', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); classUnderTest.handleError(mockRequest, mockEmptyResponse); expect(globalMessageService.add).toHaveBeenCalledTimes(0); diff --git a/feature-libs/quote/root/http-interceptors/quote-not-found.handler.spec.ts b/feature-libs/quote/root/http-interceptors/quote-not-found.handler.spec.ts index d5a40a38a07..a072f638eb7 100644 --- a/feature-libs/quote/root/http-interceptors/quote-not-found.handler.spec.ts +++ b/feature-libs/quote/root/http-interceptors/quote-not-found.handler.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpErrorResponse, HttpRequest } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { @@ -68,7 +69,7 @@ describe('QuoteBadRequestHandler', () => { }); classUnderTest = TestBed.inject(QuoteNotFoundHandler); routingService = TestBed.inject(RoutingService); - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); }); it('should be created', () => { diff --git a/feature-libs/quote/test.ts b/feature-libs/quote/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/quote/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/quote/tsconfig.spec.json b/feature-libs/quote/tsconfig.spec.json index 7568e0bba22..1173470c469 100644 --- a/feature-libs/quote/tsconfig.spec.json +++ b/feature-libs/quote/tsconfig.spec.json @@ -2,12 +2,19 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", + "module": "preserve", "strict": false, "target": "es2020", - "module": "preserve", - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/quote/vitest.config.ts b/feature-libs/quote/vitest.config.ts new file mode 100644 index 00000000000..3ce3833e2f0 --- /dev/null +++ b/feature-libs/quote/vitest.config.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/quote`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-quote.xml`, + }, + ], + ], + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module`, + }, + }, +}); diff --git a/feature-libs/requested-delivery-date/core/connectors/requested-delivery-date.connector.spec.ts b/feature-libs/requested-delivery-date/core/connectors/requested-delivery-date.connector.spec.ts index 4fe26a9ab56..e77f0e3be08 100644 --- a/feature-libs/requested-delivery-date/core/connectors/requested-delivery-date.connector.spec.ts +++ b/feature-libs/requested-delivery-date/core/connectors/requested-delivery-date.connector.spec.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { take } from 'rxjs/operators'; +import { vi } from 'vitest'; import { RequestedDeliveryDateAdapter } from './requested-delivery-date.adapter'; import { RequestedDeliveryDateConnector } from './requested-delivery-date.connector'; -import createSpy = jasmine.createSpy; const mockUserId = 'userId1'; const mockCartId = '00012345'; @@ -12,9 +12,11 @@ const mockRequestedDate = '15-09-2023'; class MockRequestedDeliveryDateAdapter implements Partial { - setRequestedDeliveryDate = createSpy( - 'RequestedDeliveryDateAdapter.setRequestedDeliveryDate' - ).and.callFake((_userId: string, _cartId: string, _date: Date) => of()); + setRequestedDeliveryDate = vi + .fn() + .mockImplementation((_userId: string, _cartId: string, _date: Date) => + of() + ); } describe('RequestedDeliveryDateConnector', () => { diff --git a/feature-libs/requested-delivery-date/core/http-interceptors/bad-request/requested-delivery-date-badrequest.handler.spec.ts b/feature-libs/requested-delivery-date/core/http-interceptors/bad-request/requested-delivery-date-badrequest.handler.spec.ts index edb25e19df4..85b84d80802 100644 --- a/feature-libs/requested-delivery-date/core/http-interceptors/bad-request/requested-delivery-date-badrequest.handler.spec.ts +++ b/feature-libs/requested-delivery-date/core/http-interceptors/bad-request/requested-delivery-date-badrequest.handler.spec.ts @@ -5,6 +5,7 @@ import { GlobalMessageType, HttpResponseStatus, } from '@spartacus/core'; +import { vi } from 'vitest'; import { RequestedDeliveryDateBadRequestHandler } from './requested-delivery-date-badrequest.handler'; class MockGlobalMessageService { @@ -51,7 +52,7 @@ describe('RequestedDeliveryDateBadRequestHandler', () => { }); it('should handle wrong date bad request', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); service.handleError(MockRequest, MockRDDBadRequestResponse); expect(globalMessageService.add).toHaveBeenCalledWith( diff --git a/feature-libs/requested-delivery-date/core/services/requested-delivery-date.service.spec.ts b/feature-libs/requested-delivery-date/core/services/requested-delivery-date.service.spec.ts index ff56059dc10..eb871d908ff 100644 --- a/feature-libs/requested-delivery-date/core/services/requested-delivery-date.service.spec.ts +++ b/feature-libs/requested-delivery-date/core/services/requested-delivery-date.service.spec.ts @@ -1,10 +1,9 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { RequestedDeliveryDateConnector } from '../connectors/requested-delivery-date.connector'; import { RequestedDeliveryDateService } from './requested-delivery-date.service'; -import createSpy = jasmine.createSpy; - const mockUserId = 'userId1'; const mockCartId = '00012345'; const mockRequestedDate = '15-09-2023'; @@ -12,7 +11,7 @@ const mockRequestedDate = '15-09-2023'; class MockRequestedDeliveryDateConnector implements Partial { - setRequestedDeliveryDate = createSpy().and.callFake(() => of()); + setRequestedDeliveryDate = vi.fn().mockImplementation(() => of()); } describe('RequestedDeliveryDateService', () => { diff --git a/feature-libs/requested-delivery-date/karma.conf.js b/feature-libs/requested-delivery-date/karma.conf.js deleted file mode 100644 index 65f64c42f54..00000000000 --- a/feature-libs/requested-delivery-date/karma.conf.js +++ /dev/null @@ -1,53 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - ], - parallelOptions: { - executors: 2, - shardStrategy: 'round-robin', - }, - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots'], - coverageReporter: { - dir: require('path').join( - __dirname, - '../../coverage/requested-delivery-date' - ), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 75, - functions: 85, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/requested-delivery-date/occ/adapters/occ-requested-delivery-date.adapter.spec.ts b/feature-libs/requested-delivery-date/occ/adapters/occ-requested-delivery-date.adapter.spec.ts index b829840951c..3c8704e4a4d 100644 --- a/feature-libs/requested-delivery-date/occ/adapters/occ-requested-delivery-date.adapter.spec.ts +++ b/feature-libs/requested-delivery-date/occ/adapters/occ-requested-delivery-date.adapter.spec.ts @@ -12,6 +12,7 @@ import { TestBed } from '@angular/core/testing'; import { HttpErrorModel, OccConfig, OccEndpoints } from '@spartacus/core'; import { throwError } from 'rxjs'; import { take } from 'rxjs/operators'; +import { vi } from 'vitest'; import { OccRequestedDeliveryDateAdapter } from './occ-requested-delivery-date.adapter'; const mockUserId = 'userId1'; @@ -74,13 +75,13 @@ describe('OccRequestedDeliveryDateAdapter', () => { }); describe(`set requested delivery date`, () => { - it(`should set requested delivery date for cart for given user id, cart id`, (done) => { + it(`should set requested delivery date for cart for given user id, cart id`, () => { + let capturedResult: any; service .setRequestedDeliveryDate(mockUserId, mockCartId, mockRequestedDate) .pipe(take(1)) .subscribe((result) => { - expect(result).toEqual(''); - done(); + capturedResult = result; }); const mockReq = httpMock.expectOne((req) => { @@ -94,10 +95,13 @@ describe('OccRequestedDeliveryDateAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); mockReq.flush(''); expect(mockReq.request.responseType).toEqual('json'); + expect(capturedResult).toEqual(''); }); it(`should result in error when Validation Error is thrown`, () => { - spyOn(httpClient, 'put').and.returnValue(throwError(mockValidationError)); + vi.spyOn(httpClient, 'put').mockReturnValue( + throwError(mockValidationError) + ); let result: HttpErrorModel | undefined; const subscription = service diff --git a/feature-libs/requested-delivery-date/project.json b/feature-libs/requested-delivery-date/project.json index 8e24751b1b1..f811e9cd288 100644 --- a/feature-libs/requested-delivery-date/project.json +++ b/feature-libs/requested-delivery-date/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/requested-delivery-date/test.ts", - "tsConfig": "feature-libs/requested-delivery-date/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/requested-delivery-date/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/requested-delivery-date/root/components/delivery-mode-date-picker/delivery-mode-date-picker.component.spec.ts b/feature-libs/requested-delivery-date/root/components/delivery-mode-date-picker/delivery-mode-date-picker.component.spec.ts index a5167b8d9c3..cc4bd8f2a9f 100644 --- a/feature-libs/requested-delivery-date/root/components/delivery-mode-date-picker/delivery-mode-date-picker.component.spec.ts +++ b/feature-libs/requested-delivery-date/root/components/delivery-mode-date-picker/delivery-mode-date-picker.component.spec.ts @@ -18,6 +18,7 @@ import { OutletContextData, } from '@spartacus/storefront'; import { of, throwError } from 'rxjs'; +import { vi } from 'vitest'; import { RequestedDeliveryDateFacade } from '../../facade/requested-delivery-date.facade'; import { DeliveryModeDatePickerComponent } from './delivery-mode-date-picker.component'; @@ -26,9 +27,7 @@ describe('DeliveryModeDatePickerComponent', () => { let fixture: ComponentFixture; const requestedDelDateFacadeMock = { - setRequestedDeliveryDate: jasmine - .createSpy('setRequestedDeliveryDate') - .and.returnValue(of({})), + setRequestedDeliveryDate: vi.fn().mockReturnValue(of({})), }; const mockedGlobalMessageService = { @@ -41,9 +40,7 @@ describe('DeliveryModeDatePickerComponent', () => { }; const translationServiceMock = { - translate: jasmine - .createSpy('translate') - .and.returnValue(of('Delivery Date')), + translate: vi.fn().mockReturnValue(of('Delivery Date')), }; beforeEach(async () => { @@ -84,14 +81,17 @@ describe('DeliveryModeDatePickerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(DeliveryModeDatePickerComponent); component = fixture.componentInstance; - fixture.detectChanges(); + // NOTE: no fixture.detectChanges() here — each test sets its own state first + // to avoid NG0100 ExpressionChangedAfterItHasBeenCheckedError }); afterEach(() => { + vi.restoreAllMocks(); fixture.destroy(); }); it('should create the component', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -109,7 +109,6 @@ describe('DeliveryModeDatePickerComponent', () => { }); const textTitle = 'Delivery Date'; - component.ngOnInit(); fixture.detectChanges(); let card: Card = {}; @@ -151,7 +150,6 @@ describe('DeliveryModeDatePickerComponent', () => { }); const datePickerLab = 'requestedDeliveryDate.datePickerLabel'; - component.ngOnInit(); fixture.detectChanges(); const datePickerLabelEl = fixture.debugElement.query( @@ -172,7 +170,7 @@ describe('DeliveryModeDatePickerComponent', () => { component['cartEntry'] = { requestedRetrievalAt, } as any; - component.ngOnInit(); + fixture.detectChanges(); expect(component['form'].get('requestDeliveryDate')?.value).toEqual( requestedRetrievalAt ); @@ -187,7 +185,7 @@ describe('DeliveryModeDatePickerComponent', () => { uid: 'current', }, } as any; - component.ngOnInit(); + fixture.detectChanges(); expect(component['requestedRetrievalAt']).toEqual(earliestRetrievalAt); expect(component['form'].get('requestDeliveryDate')?.value).toEqual( earliestRetrievalAt @@ -197,8 +195,8 @@ describe('DeliveryModeDatePickerComponent', () => { ).toHaveBeenCalled(); }); - it('should call setRequestedDeliveryDate when form value changes and show info message on success', (done) => { - spyOn(component['globalMessageService'], 'add'); + it('should call setRequestedDeliveryDate when form value changes and show info message on success', async () => { + vi.spyOn(component['globalMessageService'], 'add'); const requestedRetrievalAt = '2023-05-03'; const earliestRetrievalAt = '2023-09-15'; const data = TestBed.inject(OutletContextData); @@ -214,7 +212,6 @@ describe('DeliveryModeDatePickerComponent', () => { readonly: false, }); - component.ngOnInit(); fixture.detectChanges(); const newRequestedRetrievalAt = '2023-09-15'; component['form'].patchValue({ @@ -231,23 +228,26 @@ describe('DeliveryModeDatePickerComponent', () => { expect( component['requestedDelDateFacade'].setRequestedDeliveryDate ).toHaveBeenCalled(); - component['requestedDelDateFacade'] - .setRequestedDeliveryDate('current', '123', newRequestedRetrievalAt) - .subscribe(() => { - expect(component['globalMessageService'].add).toHaveBeenCalledWith( - { key: 'requestedDeliveryDate.successMessage' }, - GlobalMessageType.MSG_TYPE_INFO - ); - done(); - }); + + await new Promise((resolve) => { + component['requestedDelDateFacade'] + .setRequestedDeliveryDate('current', '123', newRequestedRetrievalAt) + .subscribe(() => { + expect(component['globalMessageService'].add).toHaveBeenCalledWith( + { key: 'requestedDeliveryDate.successMessage' }, + GlobalMessageType.MSG_TYPE_INFO + ); + resolve(); + }); + }); }); it('should NOT call setRequestedDeliveryDate when a date less than earliestRetrievalAt is provided', () => { - spyOn(component, 'setRequestedDeliveryDate'); + vi.spyOn(component, 'setRequestedDeliveryDate'); - component['requestedDelDateFacade'].setRequestedDeliveryDate = jasmine - .createSpy('setRequestedDeliveryDate') - .and.returnValue(of({})); + component['requestedDelDateFacade'].setRequestedDeliveryDate = vi + .fn() + .mockReturnValue(of({})); const requestedRetrievalAt = '2023-05-03'; const earliestRetrievalAt = '2023-09-15'; @@ -264,7 +264,6 @@ describe('DeliveryModeDatePickerComponent', () => { readonly: false, }); - component.ngOnInit(); fixture.detectChanges(); const newRequestedRetrievalAt = '2023-01-01'; component['form'].patchValue({ @@ -285,7 +284,7 @@ describe('DeliveryModeDatePickerComponent', () => { }); it('should NOT show the date picker when the component outlet value is read only', () => { - spyOn(component, 'setRequestedDeliveryDate'); + vi.spyOn(component, 'setRequestedDeliveryDate'); const requestedRetrievalAt = '2023-05-03'; const earliestRetrievalAt = '2023-09-15'; const data = TestBed.inject(OutletContextData); @@ -301,7 +300,6 @@ describe('DeliveryModeDatePickerComponent', () => { readonly: true, }); - component.ngOnInit(); fixture.detectChanges(); const datePickerEl: HTMLInputElement = fixture.debugElement.query( By.css('cx-date-picker') @@ -313,12 +311,12 @@ describe('DeliveryModeDatePickerComponent', () => { expect(datePickerReadOnlyEl.innerHTML).not.toBeNull(); }); - it('should show error message when backend OCC API returns UnknownResourceError', (done) => { - spyOn(component['globalMessageService'], 'add'); + it('should show error message when backend OCC API returns UnknownResourceError', async () => { + vi.spyOn(component['globalMessageService'], 'add'); - component['requestedDelDateFacade'].setRequestedDeliveryDate = jasmine - .createSpy('setRequestedDeliveryDate') - .and.returnValue( + component['requestedDelDateFacade'].setRequestedDeliveryDate = vi + .fn() + .mockReturnValue( throwError({ error: { errors: [ @@ -340,7 +338,7 @@ describe('DeliveryModeDatePickerComponent', () => { uid: 'current', }, } as any; - component.ngOnInit(); + fixture.detectChanges(); expect(component['requestedRetrievalAt']).toEqual(earliestRetrievalAt); expect(component['form'].get('requestDeliveryDate')?.value).toEqual( earliestRetrievalAt @@ -349,21 +347,24 @@ describe('DeliveryModeDatePickerComponent', () => { component['requestedDelDateFacade'].setRequestedDeliveryDate ).toHaveBeenCalled(); - component['requestedDelDateFacade'] - .setRequestedDeliveryDate('current', '123', earliestRetrievalAt) - .subscribe({ - error: () => { - expect(component['globalMessageService'].add).toHaveBeenCalledWith( - { key: 'requestedDeliveryDate.errorMessage' }, - GlobalMessageType.MSG_TYPE_ERROR - ); - done(); - }, - }); + await new Promise((resolve) => { + component['requestedDelDateFacade'] + .setRequestedDeliveryDate('current', '123', earliestRetrievalAt) + .subscribe({ + error: () => { + expect(component['globalMessageService'].add).toHaveBeenCalledWith( + { key: 'requestedDeliveryDate.errorMessage' }, + GlobalMessageType.MSG_TYPE_ERROR + ); + resolve(); + }, + }); + }); }); it('should unsubscribe from subscription on component destruction', () => { - spyOn(component['subscription'], 'unsubscribe'); + fixture.detectChanges(); + vi.spyOn(component['subscription'], 'unsubscribe'); component.ngOnDestroy(); expect(component['subscription'].unsubscribe).toHaveBeenCalled(); }); diff --git a/feature-libs/requested-delivery-date/root/components/order-overview-delivery-date/order-overview-delivery-date.component.spec.ts b/feature-libs/requested-delivery-date/root/components/order-overview-delivery-date/order-overview-delivery-date.component.spec.ts index 593038a1a84..61ccf10f601 100644 --- a/feature-libs/requested-delivery-date/root/components/order-overview-delivery-date/order-overview-delivery-date.component.spec.ts +++ b/feature-libs/requested-delivery-date/root/components/order-overview-delivery-date/order-overview-delivery-date.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { I18nTestingModule, TranslationService } from '@spartacus/core'; import { Card, OutletContextData } from '@spartacus/storefront'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { OrderOverviewDeliveryDateComponent } from './order-overview-delivery-date.component'; describe('OrderOverviewDeliveryDateComponent', () => { @@ -9,9 +10,7 @@ describe('OrderOverviewDeliveryDateComponent', () => { let fixture: ComponentFixture; const translationServiceMock = { - translate: jasmine - .createSpy('translate') - .and.returnValue(of('Translated Text')), + translate: vi.fn().mockReturnValue(of('Translated Text')), }; beforeEach(async () => { @@ -61,7 +60,7 @@ describe('OrderOverviewDeliveryDateComponent', () => { }); it('should unsubscribe from subscription on component destruction', () => { - spyOn(component['subscription'], 'unsubscribe'); + vi.spyOn(component['subscription'], 'unsubscribe'); component.ngOnDestroy(); expect(component['subscription'].unsubscribe).toHaveBeenCalled(); }); diff --git a/feature-libs/requested-delivery-date/test.ts b/feature-libs/requested-delivery-date/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/requested-delivery-date/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/requested-delivery-date/tsconfig.spec.json b/feature-libs/requested-delivery-date/tsconfig.spec.json index c18562e56f7..557f701506a 100644 --- a/feature-libs/requested-delivery-date/tsconfig.spec.json +++ b/feature-libs/requested-delivery-date/tsconfig.spec.json @@ -2,14 +2,21 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "strict": false, "module": "preserve", - "types": ["jasmine", "node"], + "strict": false, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], "skipLibCheck": true, "resolveJsonModule": true, "esModuleInterop": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/requested-delivery-date/vitest.config.ts b/feature-libs/requested-delivery-date/vitest.config.ts new file mode 100644 index 00000000000..c2e3ff1418c --- /dev/null +++ b/feature-libs/requested-delivery-date/vitest.config.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/requested-delivery-date`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-requested-delivery-date.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/smartedit/core/decorators/smart-edit-component-decorator.spec.ts b/feature-libs/smartedit/core/decorators/smart-edit-component-decorator.spec.ts index 7514a3de16e..8abdc56552d 100644 --- a/feature-libs/smartedit/core/decorators/smart-edit-component-decorator.spec.ts +++ b/feature-libs/smartedit/core/decorators/smart-edit-component-decorator.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { SmartEditService } from '../services/smart-edit.service'; import { SmartEditComponentDecorator } from './smart-edit-component-decorator'; @@ -29,7 +30,7 @@ describe('SmartEditComponentDecorator', () => { it('should call addSmartEditContract', () => { const component = { properties: { smartedit: { uuid: 'test-id' } } }; - spyOn(smartEditService, 'addSmartEditContract'); + vi.spyOn(smartEditService, 'addSmartEditContract'); decorator.decorate(null, null, component); expect(smartEditService.addSmartEditContract).toHaveBeenCalledWith( null, diff --git a/feature-libs/smartedit/core/decorators/smart-edit-slot-decorator.spec.ts b/feature-libs/smartedit/core/decorators/smart-edit-slot-decorator.spec.ts index 94e42745ab3..c9b008f1109 100644 --- a/feature-libs/smartedit/core/decorators/smart-edit-slot-decorator.spec.ts +++ b/feature-libs/smartedit/core/decorators/smart-edit-slot-decorator.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { SmartEditService } from '../services/smart-edit.service'; import { SmartEditSlotDecorator } from './smart-edit-slot-decorator'; @@ -29,7 +30,7 @@ describe('SmartEditSlotDecorator', () => { it('should call addSmartEditContract', () => { const slot = { properties: { smartedit: { uuid: 'test-id' } } }; - spyOn(smartEditService, 'addSmartEditContract'); + vi.spyOn(smartEditService, 'addSmartEditContract'); decorator.decorate(null, null, slot); expect(smartEditService.addSmartEditContract).toHaveBeenCalledWith( null, diff --git a/feature-libs/smartedit/core/services/smart-edit.service.spec.ts b/feature-libs/smartedit/core/services/smart-edit.service.spec.ts index 3188ba6fb1d..32f67b51b9d 100644 --- a/feature-libs/smartedit/core/services/smart-edit.service.spec.ts +++ b/feature-libs/smartedit/core/services/smart-edit.service.spec.ts @@ -9,6 +9,7 @@ import { ScriptLoader, } from '@spartacus/core'; import { EMPTY, Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { defaultSmartEditConfig } from '../../root/config/default-smart-edit-config'; import { SmartEditConfig } from '../../root/config/smart-edit-config'; import { SmartEditService } from './smart-edit.service'; @@ -62,8 +63,8 @@ describe('SmartEditService', () => { baseSiteService = TestBed.inject(BaseSiteService); scriptLoader = TestBed.inject(ScriptLoader); - spyOn(routingService, 'go').and.stub(); - spyOn(scriptLoader, 'embedScript').and.callThrough(); + vi.spyOn(routingService, 'go').mockImplementation(() => {}); + vi.spyOn(scriptLoader, 'embedScript'); }); it('should SmartEditService is injected', () => { @@ -72,13 +73,13 @@ describe('SmartEditService', () => { describe('should add page contract', () => { it('should add CSS classes in body tag', () => { - spyOn(baseSiteService, 'get').and.returnValue( + vi.spyOn(baseSiteService, 'get').mockReturnValue( of({ defaultPreviewProductCode: 'test product code', defaultPreviewCategoryCode: 'test category code', }) ); - spyOn(cmsService, 'getCurrentPage').and.returnValues( + vi.spyOn(cmsService, 'getCurrentPage').mockReturnValueOnce( of({ pageId: 'testPageId', properties: { @@ -149,22 +150,22 @@ describe('SmartEditService', () => { describe('should render cms components', () => { it('should render a slot (refresh page by Id)', () => { - spyOn(cmsService, 'clearComponentState').and.stub(); - spyOn(cmsService, 'refreshPageById').and.stub(); + vi.spyOn(cmsService, 'clearComponentState').mockImplementation(() => {}); + vi.spyOn(cmsService, 'refreshPageById').mockImplementation(() => {}); service['_currentPageId'] = 'testPageId'; service['renderComponent']('test-slot'); expect(cmsService.clearComponentState).toHaveBeenCalledWith(); expect(cmsService.refreshPageById).toHaveBeenCalledWith('testPageId'); }); it('should render a slot (refresh latest page)', () => { - spyOn(cmsService, 'clearComponentState').and.stub(); - spyOn(cmsService, 'refreshLatestPage').and.stub(); + vi.spyOn(cmsService, 'clearComponentState').mockImplementation(() => {}); + vi.spyOn(cmsService, 'refreshLatestPage').mockImplementation(() => {}); service['renderComponent']('test-slot'); expect(cmsService.clearComponentState).toHaveBeenCalledWith(); expect(cmsService.refreshLatestPage).toHaveBeenCalled(); }); it('should render a component', () => { - spyOn(cmsService, 'refreshComponent').and.stub(); + vi.spyOn(cmsService, 'refreshComponent').mockImplementation(() => {}); service['renderComponent']('test-component', 'banner', 'test-slot'); expect(cmsService.refreshComponent).toHaveBeenCalledWith( 'test-component' diff --git a/feature-libs/smartedit/karma.conf.js b/feature-libs/smartedit/karma.conf.js deleted file mode 100644 index cdcec31e463..00000000000 --- a/feature-libs/smartedit/karma.conf.js +++ /dev/null @@ -1,52 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-smartedit.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/smartedit'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 80, - functions: 75, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/smartedit/project.json b/feature-libs/smartedit/project.json index ea87be17da5..3b90e39f2f8 100644 --- a/feature-libs/smartedit/project.json +++ b/feature-libs/smartedit/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/smartedit/test.ts", - "tsConfig": "feature-libs/smartedit/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/smartedit/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/smartedit/root/http-interceptors/cms-ticket.interceptor.spec.ts b/feature-libs/smartedit/root/http-interceptors/cms-ticket.interceptor.spec.ts index 512f68c9fb5..220f0d2a29b 100644 --- a/feature-libs/smartedit/root/http-interceptors/cms-ticket.interceptor.spec.ts +++ b/feature-libs/smartedit/root/http-interceptors/cms-ticket.interceptor.spec.ts @@ -16,6 +16,7 @@ import { defaultOccConfig, } from '@spartacus/core'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { SmartEditLauncherService } from '../services/smart-edit-launcher.service'; import { CmsTicketInterceptor } from './cms-ticket.interceptor'; @@ -90,7 +91,7 @@ describe('CmsTicketInterceptor', () => { it('should add parameters only for cms requests: cmsTicketId', inject( [HttpClient], (http: HttpClient) => { - spyOnProperty(service, 'cmsTicketId', 'get').and.returnValue( + vi.spyOn(service, 'cmsTicketId', 'get').mockReturnValue( 'mockCmsTicketId' ); @@ -111,7 +112,7 @@ describe('CmsTicketInterceptor', () => { it('should not add parameters to other requests: cmsTicketId', inject( [HttpClient], (http: HttpClient) => { - spyOnProperty(service, 'cmsTicketId', 'get').and.returnValue( + vi.spyOn(service, 'cmsTicketId', 'get').mockReturnValue( 'mockCmsTicketId' ); @@ -131,7 +132,7 @@ describe('CmsTicketInterceptor', () => { it('should add parameters for product requests: cmsTicketId', inject( [HttpClient], (http: HttpClient) => { - spyOnProperty(service, 'cmsTicketId', 'get').and.returnValue( + vi.spyOn(service, 'cmsTicketId', 'get').mockReturnValue( 'mockCmsTicketId' ); @@ -152,7 +153,7 @@ describe('CmsTicketInterceptor', () => { it('should add parameters for productList requests: cmsTicketId, categoryCode', inject( [HttpClient], (http: HttpClient) => { - spyOnProperty(service, 'cmsTicketId', 'get').and.returnValue( + vi.spyOn(service, 'cmsTicketId', 'get').mockReturnValue( 'mockCmsTicketId' ); @@ -174,14 +175,14 @@ describe('CmsTicketInterceptor', () => { it('should add only one parameter for productList requests when pageContext is partial: cmsTicketId', inject( [HttpClient], (http: HttpClient) => { - spyOn(routingService, 'getPageContext').and.returnValue( + vi.spyOn(routingService, 'getPageContext').mockReturnValue( of({ ...mockPageContext, id: '', }) ); - spyOnProperty(service, 'cmsTicketId', 'get').and.returnValue( + vi.spyOn(service, 'cmsTicketId', 'get').mockReturnValue( 'mockCmsTicketId' ); diff --git a/feature-libs/smartedit/root/services/smart-edit-launcher.service.spec.ts b/feature-libs/smartedit/root/services/smart-edit-launcher.service.spec.ts index 849b8d64e25..dcd20f1ef2d 100644 --- a/feature-libs/smartedit/root/services/smart-edit-launcher.service.spec.ts +++ b/feature-libs/smartedit/root/services/smart-edit-launcher.service.spec.ts @@ -6,6 +6,7 @@ import { WindowRef, } from '@spartacus/core'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { defaultSmartEditConfig } from '../config/default-smart-edit-config'; import { SmartEditConfig } from '../config/smart-edit-config'; import { SmartEditLauncherService } from './smart-edit-launcher.service'; @@ -67,7 +68,7 @@ describe('SmartEditLauncherService', () => { describe('should get whether Spartacus is launched in SmartEdit', () => { it('launched in smartEdit when storefrontPreviewRoute matches, and there is cmsTicketId', () => { - spyOn(location, 'path').and.returnValue( + vi.spyOn(location, 'path').mockReturnValue( '/any/cx-preview?cmsTicketId=test-cms-ticket-id' ); const launched = smartEditLauncherService.isLaunchedInSmartEdit(); @@ -75,7 +76,7 @@ describe('SmartEditLauncherService', () => { }); it('not launched in smartEdit when storefrontPreviewRoute does not matches', () => { - spyOn(location, 'path').and.returnValue( + vi.spyOn(location, 'path').mockReturnValue( '/any/cx-something?cmsTicketId=test-cms-ticket-id' ); const launched = smartEditLauncherService.isLaunchedInSmartEdit(); @@ -83,13 +84,13 @@ describe('SmartEditLauncherService', () => { }); it('not launched in smartEdit when there is no cmsTicketId', () => { - spyOn(location, 'path').and.returnValue('/any/cx-preview'); + vi.spyOn(location, 'path').mockReturnValue('/any/cx-preview'); const launched = smartEditLauncherService.isLaunchedInSmartEdit(); expect(launched).toBeFalsy(); }); it('should persist cmsTicketId to sessionStorage on initial SmartEdit launch', () => { - spyOn(location, 'path').and.returnValue( + vi.spyOn(location, 'path').mockReturnValue( '/any/cx-preview?cmsTicketId=abc123' ); expect(sessionStorage.getItem('smartedit.cmsTicketId')).toBeNull(); @@ -100,10 +101,9 @@ describe('SmartEditLauncherService', () => { }); it('should restore cmsTicketId from sessionStorage when full page redirect occurs after initial launch', () => { - spyOn(location, 'path').and.returnValues( - '/any/cx-preview?cmsTicketId=abc123', - '/any/login/callback?code=auth-code' - ); + vi.spyOn(location, 'path') + .mockReturnValueOnce('/any/cx-preview?cmsTicketId=abc123') + .mockReturnValueOnce('/any/login/callback?code=auth-code'); expect(sessionStorage.getItem('smartedit.cmsTicketId')).toBeNull(); const launched = smartEditLauncherService.isLaunchedInSmartEdit(); expect(launched).toBeTruthy(); @@ -115,10 +115,9 @@ describe('SmartEditLauncherService', () => { }); it('should prefer cmsTicketId from URL over sessionStorage', () => { - spyOn(location, 'path').and.returnValues( - '/any/cx-preview?cmsTicketId=abc123', - '/any/cx-preview?cmsTicketId=def456' - ); + vi.spyOn(location, 'path') + .mockReturnValueOnce('/any/cx-preview?cmsTicketId=abc123') + .mockReturnValueOnce('/any/cx-preview?cmsTicketId=def456'); expect(sessionStorage.getItem('smartedit.cmsTicketId')).toBeNull(); const launched = smartEditLauncherService.isLaunchedInSmartEdit(); expect(launched).toBeTruthy(); @@ -132,10 +131,10 @@ describe('SmartEditLauncherService', () => { describe('should lazy load SmartEditModule', () => { it('lazy load SmartEditModule', () => { - spyOn(location, 'path').and.returnValue( + vi.spyOn(location, 'path').mockReturnValue( '/any/cx-preview?cmsTicketId=test-cms-ticket-id' ); - spyOn(featureModules, 'resolveFeature').and.callThrough(); + vi.spyOn(featureModules, 'resolveFeature'); smartEditLauncherService.load(); expect(featureModules.resolveFeature).toHaveBeenCalledWith('smartEdit'); @@ -143,10 +142,10 @@ describe('SmartEditLauncherService', () => { }); it('should be able to load webApplicationInjector.js', () => { - spyOn(location, 'path').and.returnValue( + vi.spyOn(location, 'path').mockReturnValue( '/any/cx-preview?cmsTicketId=test-cms-ticket-id' ); - spyOn(scriptLoader, 'embedScript').and.callThrough(); + vi.spyOn(scriptLoader, 'embedScript'); smartEditLauncherService.load(); expect(scriptLoader.embedScript).toHaveBeenCalled(); @@ -154,9 +153,9 @@ describe('SmartEditLauncherService', () => { describe('SSR behavior', () => { it('should not read cmsTicketId from sessionStorage when not in browser', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); sessionStorage.setItem('smartedit.cmsTicketId', 'abc123'); - spyOn(location, 'path').and.returnValue( + vi.spyOn(location, 'path').mockReturnValue( '/any/login/callback?code=auth-code' ); @@ -167,8 +166,8 @@ describe('SmartEditLauncherService', () => { }); it('should not store cmsTicketId in sessionStorage when not in browser', () => { - spyOn(windowRef, 'isBrowser').and.returnValue(false); - spyOn(location, 'path').and.returnValue( + vi.spyOn(windowRef, 'isBrowser').mockReturnValue(false); + vi.spyOn(location, 'path').mockReturnValue( '/any/cx-preview?cmsTicketId=abc123' ); diff --git a/feature-libs/smartedit/test.ts b/feature-libs/smartedit/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/smartedit/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/smartedit/tsconfig.spec.json b/feature-libs/smartedit/tsconfig.spec.json index 34d8415e3a6..d52c68cbde6 100644 --- a/feature-libs/smartedit/tsconfig.spec.json +++ b/feature-libs/smartedit/tsconfig.spec.json @@ -4,9 +4,16 @@ "outDir": "../../out-tsc/spec", "module": "preserve", "strict": false, - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/smartedit/vitest.config.ts b/feature-libs/smartedit/vitest.config.ts new file mode 100644 index 00000000000..9502b45e338 --- /dev/null +++ b/feature-libs/smartedit/vitest.config.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/smartedit`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-smartedit.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/storefinder/components/store-finder-grid/store-finder-grid.component.spec.ts b/feature-libs/storefinder/components/store-finder-grid/store-finder-grid.component.spec.ts index bf8eea81af9..e6839c8678b 100644 --- a/feature-libs/storefinder/components/store-finder-grid/store-finder-grid.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-grid/store-finder-grid.component.spec.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; +import { vi } from 'vitest'; import { MockTranslatePipe, RoutingService, @@ -11,7 +12,6 @@ import { StoreFinderService } from '@spartacus/storefinder/core'; import { EMPTY, Observable } from 'rxjs'; import { StoreFinderGridComponent } from './store-finder-grid.component'; import { StoreFinderListItemComponent } from '../store-finder-list-item/store-finder-list-item.component'; -import createSpy = jasmine.createSpy; const countryIsoCode = 'CA'; const regionIsoCode = 'CA-QC'; @@ -38,15 +38,13 @@ const mockActivatedRoute = { }; class MockStoreFinderService implements Partial { - getFindStoresEntities = createSpy('getFindStoresEntities').and.returnValue( - EMPTY - ); - getStoresLoading = createSpy('getStoresLoading'); - callFindStoresAction = createSpy('callFindStoresAction'); + getFindStoresEntities = vi.fn().mockReturnValue(EMPTY); + getStoresLoading = vi.fn(); + callFindStoresAction = vi.fn(); } const mockRoutingService = { - go: createSpy('go'), + go: vi.fn(), }; describe('StoreFinderGridComponent', () => { @@ -55,7 +53,7 @@ describe('StoreFinderGridComponent', () => { let storeFinderService: StoreFinderService; let route: ActivatedRoute; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreFinderGridComponent], providers: [ @@ -74,7 +72,7 @@ describe('StoreFinderGridComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderGridComponent); diff --git a/feature-libs/storefinder/components/store-finder-header/store-finder-header.component.spec.ts b/feature-libs/storefinder/components/store-finder-header/store-finder-header.component.spec.ts index 8281417d6ca..b1d0c5d6420 100644 --- a/feature-libs/storefinder/components/store-finder-header/store-finder-header.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-header/store-finder-header.component.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CxDatePipe, MockDatePipe, @@ -21,7 +21,7 @@ describe('StoreFinderHeaderComponent', () => { let component: StoreFinderHeaderComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreFinderHeaderComponent], providers: [ @@ -41,7 +41,7 @@ describe('StoreFinderHeaderComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderHeaderComponent); diff --git a/feature-libs/storefinder/components/store-finder-list-item/store-finder-list-item.component.spec.ts b/feature-libs/storefinder/components/store-finder-list-item/store-finder-list-item.component.spec.ts index 9a7a7386164..b40cbda9eb5 100644 --- a/feature-libs/storefinder/components/store-finder-list-item/store-finder-list-item.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-list-item/store-finder-list-item.component.spec.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { provideLocationMocks } from '@angular/common/testing'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; @@ -8,8 +8,8 @@ import { I18nTestingModule } from '@spartacus/core'; import { StoreFinderService } from '@spartacus/storefinder/core'; import { OutletModule } from '@spartacus/storefront'; import { EMPTY } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderListItemComponent } from './store-finder-list-item.component'; -import createSpy = jasmine.createSpy; const weekday = { closingTime: { @@ -88,21 +88,19 @@ const sampleStore: any = { }; class MockStoreFinderService implements Partial { - getFindStoresEntities = createSpy('getFindStoresEntities').and.returnValue( - EMPTY - ); - getStoresLoading = createSpy('getStoresLoading'); - callFindStoresAction = createSpy('callFindStoresAction'); - getStoreLatitude = createSpy('getStoreLatitude'); - getStoreLongitude = createSpy('getStoreLongitude'); - getDirections = createSpy('getDirections'); + getFindStoresEntities = vi.fn().mockReturnValue(EMPTY); + getStoresLoading = vi.fn(); + callFindStoresAction = vi.fn(); + getStoreLatitude = vi.fn(); + getStoreLongitude = vi.fn(); + getDirections = vi.fn(); } describe('StoreFinderListItemComponent', () => { let component: StoreFinderListItemComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ CommonModule, @@ -117,7 +115,7 @@ describe('StoreFinderListItemComponent', () => { { provide: StoreFinderService, useClass: MockStoreFinderService }, ], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderListItemComponent); @@ -132,7 +130,7 @@ describe('StoreFinderListItemComponent', () => { }); it('should emit item index', () => { - spyOn(component.storeItemClick, 'emit'); + vi.spyOn(component.storeItemClick, 'emit'); component.handleStoreItemClick(); fixture.detectChanges(); expect(component.storeItemClick.emit).toHaveBeenCalledWith(1); @@ -143,7 +141,9 @@ describe('StoreFinderListItemComponent', () => { const encodedName = name.replace(' ', '%20'); const link = fixture.debugElement .queryAll(By.css('.cx-store-name')) - .find((el) => el.nativeElement.innerText === displayName)?.nativeElement; + .find( + (el) => el.nativeElement.textContent?.trim() === displayName + )?.nativeElement; expect(link.getAttribute('href')).toEqual(`/${encodedName}`); }); }); diff --git a/feature-libs/storefinder/components/store-finder-map/store-finder-map.component.spec.ts b/feature-libs/storefinder/components/store-finder-map/store-finder-map.component.spec.ts index feff9c8d7b1..6a60daefe4b 100644 --- a/feature-libs/storefinder/components/store-finder-map/store-finder-map.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-map/store-finder-map.component.spec.ts @@ -1,6 +1,7 @@ import { DebugElement, ElementRef, SimpleChange } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GoogleMapRendererService } from '@spartacus/storefinder/core'; +import { vi } from 'vitest'; import { StoreFinderMapComponent } from './store-finder-map.component'; class MapRendererServiceMock { @@ -40,7 +41,7 @@ describe('StoreFinderMapComponent', () => { it('should render map', () => { // given - spyOn(mapRendererService, 'renderMap'); + vi.spyOn(mapRendererService, 'renderMap'); // when locations are changed component.locations = [location]; @@ -52,13 +53,13 @@ describe('StoreFinderMapComponent', () => { expect(mapRendererService.renderMap).toHaveBeenCalledWith( mapDomElement, [location], - jasmine.any(Function) + expect.any(Function) ); }); it('should not render map when locations are not changed', () => { // given - spyOn(mapRendererService, 'renderMap'); + vi.spyOn(mapRendererService, 'renderMap'); // when locations are changed component.locations = [location]; @@ -71,7 +72,7 @@ describe('StoreFinderMapComponent', () => { }); it('should center map', () => { - spyOn(mapRendererService, 'centerMap'); + vi.spyOn(mapRendererService, 'centerMap'); component.centerMap(0, 0); expect(mapRendererService.centerMap).toHaveBeenCalledWith(0, 0); }); diff --git a/feature-libs/storefinder/components/store-finder-pagination-details/store-finder-pagination-details.component.spec.ts b/feature-libs/storefinder/components/store-finder-pagination-details/store-finder-pagination-details.component.spec.ts index 3f32fa6b494..f3feb99fc79 100644 --- a/feature-libs/storefinder/components/store-finder-pagination-details/store-finder-pagination-details.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-pagination-details/store-finder-pagination-details.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { I18nTestingModule, PaginationModel } from '@spartacus/core'; import { StoreFinderPaginationDetailsComponent } from './store-finder-pagination-details.component'; @@ -13,29 +13,30 @@ describe('StoreFinderPaginationDetailsComponent', () => { let component: StoreFinderPaginationDetailsComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [I18nTestingModule, StoreFinderPaginationDetailsComponent], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderPaginationDetailsComponent); component = fixture.componentInstance; component.pagination = mockPagination; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); it('should display proper pagination results info', () => { + fixture.detectChanges(); const detailsElement = fixture.debugElement.query( By.css('.cx-pagination-details') ).nativeElement; - expect(detailsElement.innerText).toContain( + expect(detailsElement.textContent?.trim()).toContain( `1 - ${component.pagination.pageSize} storeFinder.fromStoresFound count:${component.pagination.totalResults}` ); }); @@ -48,7 +49,7 @@ describe('StoreFinderPaginationDetailsComponent', () => { By.css('.cx-pagination-details') ).nativeElement; - expect(detailsElement.innerText).toContain( + expect(detailsElement.textContent?.trim()).toContain( `1 - ${component.pagination.totalResults} storeFinder.fromStoresFound count:${component.pagination.totalResults}` ); }); @@ -62,7 +63,7 @@ describe('StoreFinderPaginationDetailsComponent', () => { By.css('.cx-pagination-details') ).nativeElement; - expect(detailsElement.innerText).toContain( + expect(detailsElement.textContent?.trim()).toContain( `1 - ${component.pagination.totalResults} storeFinder.fromStoresFound count:${component.pagination.totalResults}` ); }); diff --git a/feature-libs/storefinder/components/store-finder-search-result/store-finder-list/store-finder-list.component.spec.ts b/feature-libs/storefinder/components/store-finder-search-result/store-finder-list/store-finder-list.component.spec.ts index 6581f8664ef..8958c52138b 100644 --- a/feature-libs/storefinder/components/store-finder-search-result/store-finder-list/store-finder-list.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-search-result/store-finder-list/store-finder-list.component.spec.ts @@ -4,7 +4,7 @@ import { } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { FeatureToggles, @@ -20,11 +20,11 @@ import { } from '@spartacus/storefinder/core'; import { SpinnerModule } from '@spartacus/storefront'; import { EMPTY } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderMapComponent } from '../../store-finder-map/store-finder-map.component'; import { StoreFinderListComponent } from './store-finder-list.component'; import { LocationDisplayMode } from './store-finder-list.model'; import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/feature-toggles/testing'; -import createSpy = jasmine.createSpy; const location: PointOfService = { displayName: 'Test Store', @@ -34,11 +34,9 @@ const locations = { stores: stores, pagination: { currentPage: 0 } }; const displayModes = LocationDisplayMode; class StoreFinderServiceMock implements Partial { - getFindStoresEntities = createSpy('getFindStoresEntities').and.returnValue( - EMPTY - ); - getStoresLoading = createSpy('getStoresLoading'); - callFindStoresAction = createSpy('callFindStoresAction'); + getFindStoresEntities = vi.fn().mockReturnValue(EMPTY); + getStoresLoading = vi.fn(); + callFindStoresAction = vi.fn(); getStoreLatitude(_location: any): number { return 35.528984; } @@ -68,7 +66,7 @@ describe('StoreFinderListComponent', () => { let storeFinderService: StoreFinderService; let googleMapRendererService: GoogleMapRendererService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], imports: [ @@ -93,7 +91,7 @@ describe('StoreFinderListComponent', () => { add: { imports: [MockTranslatePipe] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderListComponent); @@ -101,14 +99,17 @@ describe('StoreFinderListComponent', () => { storeFinderService = TestBed.inject(StoreFinderService); googleMapRendererService = TestBed.inject(GoogleMapRendererService); - spyOn(storeFinderService, 'getStoreLatitude'); - spyOn(storeFinderService, 'getStoreLongitude'); - spyOn(googleMapRendererService, 'centerMap'); + vi.spyOn(storeFinderService, 'getStoreLatitude'); + vi.spyOn(storeFinderService, 'getStoreLongitude'); + vi.spyOn(googleMapRendererService, 'centerMap'); + }); - fixture.detectChanges(); + afterEach(() => { + vi.restoreAllMocks(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); @@ -118,7 +119,7 @@ describe('StoreFinderListComponent', () => { storeMapComponent = fixture.debugElement.query( By.css('cx-store-finder-map') ).componentInstance; - spyOn(storeMapComponent, 'centerMap').and.callThrough(); + vi.spyOn(storeMapComponent, 'centerMap'); component.centerStoreOnMapByIndex(0, location); @@ -128,10 +129,13 @@ describe('StoreFinderListComponent', () => { }); it('should select store from list', () => { + fixture.detectChanges(); const itemNumber = 4; const storeListItemMock = { scrollIntoView: function () {} }; - spyOn(document, 'getElementById').and.returnValue(storeListItemMock as any); - spyOn(storeListItemMock, 'scrollIntoView'); + vi.spyOn(document, 'getElementById').mockReturnValue( + storeListItemMock as any + ); + vi.spyOn(storeListItemMock, 'scrollIntoView'); component.selectStoreItemList(itemNumber); @@ -141,21 +145,22 @@ describe('StoreFinderListComponent', () => { it('should show store details', () => { component.locations = locations; + component.storeDetails = location; // initialize binding to avoid NG0100 fixture.detectChanges(); + component.storeDetails = undefined; expect(component.isDetailsModeVisible).toBe(false); component.centerStoreOnMapByIndex(0, location); - fixture.detectChanges(); expect(component.isDetailsModeVisible).toBe(true); expect(component.storeDetails).not.toBe(null); }); it('should close store details', () => { component.locations = locations; + component.storeDetails = location; // initialize binding to avoid NG0100 fixture.detectChanges(); component.centerStoreOnMapByIndex(0, location); - fixture.detectChanges(); expect(component.isDetailsModeVisible).toBe(true); component.hideStoreDetails(); @@ -163,12 +168,14 @@ describe('StoreFinderListComponent', () => { }); it('should "setDisplayMode" switch active display mode', () => { + fixture.detectChanges(); expect(component.activeDisplayMode).toBe(displayModes.LIST_VIEW); component.setDisplayMode(displayModes.MAP_VIEW); expect(component.activeDisplayMode).toBe(displayModes.MAP_VIEW); }); it('should "isDisplayModeActive" return valid boolean flag', () => { + fixture.detectChanges(); component.setDisplayMode(displayModes.MAP_VIEW); expect(component.isDisplayModeActive(displayModes.MAP_VIEW)).toBeTruthy(); @@ -177,7 +184,6 @@ describe('StoreFinderListComponent', () => { it('should focus the back button when store details are shown', () => { component.locations = locations; - fixture.detectChanges(); component.showStoreDetails(location); fixture.detectChanges(); @@ -186,6 +192,6 @@ describe('StoreFinderListComponent', () => { By.css('.cx-back') )?.nativeElement; expect(backButton).toBeDefined(); - expect(document.activeElement).toBe(backButton); + expect(document.activeElement).toContain(backButton); }); }); diff --git a/feature-libs/storefinder/components/store-finder-search-result/store-finder-search-result.component.spec.ts b/feature-libs/storefinder/components/store-finder-search-result/store-finder-search-result.component.spec.ts index f3ce8477fee..f8992d83499 100644 --- a/feature-libs/storefinder/components/store-finder-search-result/store-finder-search-result.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-search-result/store-finder-search-result.component.spec.ts @@ -1,5 +1,5 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { MockTranslatePipe, @@ -12,6 +12,7 @@ import { StoreFinderService, } from '@spartacus/storefinder/core'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderSearchResultComponent } from './store-finder-search-result.component'; class ActivatedRouteMock { @@ -26,9 +27,9 @@ class ActivatedRouteMock { const queryText = 'query-text'; const mockStoreFinderService = { - getStoresLoading: jasmine.createSpy(), - getFindStoresEntities: jasmine.createSpy().and.returnValue(of(Observable)), - findStoresAction: jasmine.createSpy().and.returnValue(of({})), + getStoresLoading: vi.fn(), + getFindStoresEntities: vi.fn().mockReturnValue(of(Observable)), + findStoresAction: vi.fn().mockReturnValue(of({})), }; const mockStoreFinderConfig = { @@ -43,7 +44,7 @@ describe('StoreFinderListComponent', () => { let storeFinderService: StoreFinderService; let activatedRoute: ActivatedRoute | ActivatedRouteMock; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreFinderSearchResultComponent], schemas: [NO_ERRORS_SCHEMA], @@ -59,7 +60,7 @@ describe('StoreFinderListComponent', () => { add: { imports: [MockTranslatePipe] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderSearchResultComponent); diff --git a/feature-libs/storefinder/components/store-finder-search/store-finder-search.component.spec.ts b/feature-libs/storefinder/components/store-finder-search/store-finder-search.component.spec.ts index 4ff60ab8201..5f48738f419 100644 --- a/feature-libs/storefinder/components/store-finder-search/store-finder-search.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-search/store-finder-search.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { FeatureDirective, @@ -9,6 +9,7 @@ import { } from '@spartacus/core'; import { IconComponent, ICON_TYPE } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; +import { vi } from 'vitest'; import { StoreFinderSearchComponent } from './store-finder-search.component'; const query = { @@ -50,13 +51,13 @@ describe('StoreFinderSearchComponent', () => { let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreFinderSearchComponent, MockUrlPipe], providers: [ { provide: RoutingService, - useValue: { go: jasmine.createSpy() }, + useValue: { go: vi.fn() }, }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, ], @@ -72,7 +73,7 @@ describe('StoreFinderSearchComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderSearchComponent); @@ -119,7 +120,7 @@ describe('StoreFinderSearchComponent', () => { }); it('should call findStores if search value provided and Enter is an event', () => { - spyOn(component, 'findStores'); + vi.spyOn(component, 'findStores'); component.searchBox.setValue(query.queryParams.query); component.onKey(keyEvent); expect(component.findStores).toHaveBeenCalledWith(query.queryParams.query); diff --git a/feature-libs/storefinder/components/store-finder-store-description/store-finder-store-description.component.spec.ts b/feature-libs/storefinder/components/store-finder-store-description/store-finder-store-description.component.spec.ts index b5117685978..b0f94b3e1af 100644 --- a/feature-libs/storefinder/components/store-finder-store-description/store-finder-store-description.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-store-description/store-finder-store-description.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockTranslatePipe, TranslatePipe } from '@spartacus/core'; import { StoreFinderService } from '@spartacus/storefinder/core'; import { ScheduleComponent } from '../schedule-component/schedule.component'; @@ -31,7 +31,7 @@ describe('StoreFinderStoreDescriptionComponent', () => { let component: StoreFinderStoreDescriptionComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreFinderStoreDescriptionComponent], providers: [ @@ -51,7 +51,7 @@ describe('StoreFinderStoreDescriptionComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderStoreDescriptionComponent); diff --git a/feature-libs/storefinder/components/store-finder-store/store-finder-store.component.spec.ts b/feature-libs/storefinder/components/store-finder-store/store-finder-store.component.spec.ts index 59d8c393279..327ab86a509 100644 --- a/feature-libs/storefinder/components/store-finder-store/store-finder-store.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-store/store-finder-store.component.spec.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { MockTranslatePipe, @@ -12,16 +12,14 @@ import { import { StoreFinderService } from '@spartacus/storefinder/core'; import { ICON_TYPE, IconComponent, SpinnerModule } from '@spartacus/storefront'; import { EMPTY } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderStoreDescriptionComponent } from '../store-finder-store-description/store-finder-store-description.component'; import { StoreFinderStoreComponent } from './store-finder-store.component'; -import createSpy = jasmine.createSpy; class MockStoreFinderService implements Partial { - getStoresLoading = createSpy('getStoresLoading'); - getFindStoreEntityById = createSpy('getFindStoreEntityById').and.returnValue( - EMPTY - ); - viewStoreById = createSpy('viewStoreById'); + getStoresLoading = vi.fn(); + getFindStoreEntityById = vi.fn().mockReturnValue(EMPTY); + viewStoreById = vi.fn(); } @Component({ @@ -55,12 +53,12 @@ describe('StoreFinderStoreComponent', () => { let fixture: ComponentFixture; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [SpinnerModule, StoreFinderStoreComponent], providers: [ { provide: TranslationService, useClass: MockTranslationService }, - { provide: RoutingService, useValue: { go: jasmine.createSpy() } }, + { provide: RoutingService, useValue: { go: vi.fn() } }, { provide: StoreFinderService, useClass: MockStoreFinderService, @@ -88,7 +86,7 @@ describe('StoreFinderStoreComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { routingService = TestBed.inject(RoutingService); diff --git a/feature-libs/storefinder/components/store-finder-stores-count/store-finder-stores-count.component.spec.ts b/feature-libs/storefinder/components/store-finder-stores-count/store-finder-stores-count.component.spec.ts index 5b23a0f3a2c..6f17ed71d45 100644 --- a/feature-libs/storefinder/components/store-finder-stores-count/store-finder-stores-count.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder-stores-count/store-finder-stores-count.component.spec.ts @@ -1,5 +1,5 @@ import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { @@ -16,8 +16,8 @@ import { StoreFinderService } from '@spartacus/storefinder/core'; import { SpinnerModule } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderStoresCountComponent } from './store-finder-stores-count.component'; -import createSpy = jasmine.createSpy; const mockLocation = { isoCode: 'US', @@ -25,11 +25,9 @@ const mockLocation = { count: 50, }; class MockStoreFinderService implements Partial { - viewAllStores = createSpy('viewAllStores'); - getViewAllStoresEntities = createSpy( - 'getViewAllStoresEntities' - ).and.returnValue(of([mockLocation])); - getViewAllStoresLoading = createSpy('getViewAllStoresLoading'); + viewAllStores = vi.fn(); + getViewAllStoresEntities = vi.fn().mockReturnValue(of([mockLocation])); + getViewAllStoresLoading = vi.fn(); } class MockRoutingService implements Partial { @@ -42,7 +40,7 @@ describe('StoreFinderStoresCountComponent', () => { let el: DebugElement; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ SpinnerModule, @@ -70,7 +68,7 @@ describe('StoreFinderStoresCountComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderStoresCountComponent); @@ -79,7 +77,7 @@ describe('StoreFinderStoresCountComponent', () => { routingService = TestBed.inject(RoutingService); fixture.detectChanges(); - spyOn(routingService, 'go').and.callThrough(); + vi.spyOn(routingService, 'go'); }); it('should create', () => { @@ -95,7 +93,7 @@ describe('StoreFinderStoresCountComponent', () => { }); it('should handle space key to navigate to country from keyboard', () => { - spyOn(component, 'navigateToLocation').and.callThrough(); + vi.spyOn(component, 'navigateToLocation'); const countryBtn = el.query( By.css('.btn-link[aria-label="United States(50)"]') ); @@ -103,7 +101,7 @@ describe('StoreFinderStoresCountComponent', () => { const event = new KeyboardEvent('keydown', { key: ' ', }); - spyOn(event, 'preventDefault'); + vi.spyOn(event, 'preventDefault'); countryBtn.nativeElement.dispatchEvent(event); expect(component.navigateToLocation).toHaveBeenCalledWith('US', event); expect(routingService.go).toHaveBeenCalledWith([ diff --git a/feature-libs/storefinder/components/store-finder/store-finder.component.spec.ts b/feature-libs/storefinder/components/store-finder/store-finder.component.spec.ts index 3189c2b4193..7f33e85e8f4 100644 --- a/feature-libs/storefinder/components/store-finder/store-finder.component.spec.ts +++ b/feature-libs/storefinder/components/store-finder/store-finder.component.spec.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { StoreFinderHeaderComponent } from '../public_api'; import { StoreFinderComponent } from './store-finder.component'; @@ -13,7 +13,7 @@ describe('StoreFinderComponent', () => { let component: StoreFinderComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [StoreFinderComponent], }) @@ -26,7 +26,7 @@ describe('StoreFinderComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(StoreFinderComponent); diff --git a/feature-libs/storefinder/core/connectors/store-finder.connector.spec.ts b/feature-libs/storefinder/core/connectors/store-finder.connector.spec.ts index 68f497adadf..eb5f1c416e6 100644 --- a/feature-libs/storefinder/core/connectors/store-finder.connector.spec.ts +++ b/feature-libs/storefinder/core/connectors/store-finder.connector.spec.ts @@ -1,20 +1,14 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderAdapter } from './store-finder.adapter'; import { StoreFinderConnector } from './store-finder.connector'; -import createSpy = jasmine.createSpy; import { GeoPoint, SearchConfig } from '@spartacus/core'; class MockStoreFinderAdapter implements StoreFinderAdapter { - search = createSpy('adapter.search').and.returnValue( - of(`adapter.search result`) - ); - - load = createSpy('adapter.load').and.returnValue(of(`adapter.load result`)); - - loadCounts = createSpy('adapter.loadCounts').and.returnValue( - of(`adapter.loadCounts result`) - ); + search = vi.fn().mockReturnValue(of(`adapter.search result`)); + load = vi.fn().mockReturnValue(of(`adapter.load result`)); + loadCounts = vi.fn().mockReturnValue(of(`adapter.loadCounts result`)); } describe('StoreFinderConnector', () => { diff --git a/feature-libs/storefinder/core/facade/store-finder.service.spec.ts b/feature-libs/storefinder/core/facade/store-finder.service.spec.ts index f484445ac72..8c6433f4e1a 100644 --- a/feature-libs/storefinder/core/facade/store-finder.service.spec.ts +++ b/feature-libs/storefinder/core/facade/store-finder.service.spec.ts @@ -1,6 +1,6 @@ import { inject, TestBed } from '@angular/core/testing'; -import * as NgrxStore from '@ngrx/store'; -import { MemoizedSelector, Store, StoreModule } from '@ngrx/store'; +import { MemoizedSelector } from '@ngrx/store'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { GeoPoint, GlobalMessageService, @@ -8,16 +8,14 @@ import { RoutingService, WindowRef, } from '@spartacus/core'; -import { BehaviorSubject, EMPTY } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderConfig } from '../config/store-finder-config'; import { StoreFinderSelectors } from '../store'; import { StoreFinderActions } from '../store/actions/index'; -import * as fromStoreReducers from '../store/reducers/index'; import { FindStoresState, StateWithStoreFinder, - StoresState, - STORE_FINDER_FEATURE, } from '../store/store-finder-state'; import { StoreFinderService } from './store-finder.service'; @@ -138,7 +136,7 @@ const location: PointOfService = { describe('StoreFinderService', () => { let service: StoreFinderService; - let store: Store; + let store: MockStore; let winRef: WindowRef; let routingService: RoutingService; @@ -170,62 +168,58 @@ describe('StoreFinderService', () => { findStoresEntities: { pointOfServices: [] }, findStoreEntityById: {}, }; - const storeLoading$: BehaviorSubject = new BehaviorSubject(true); - const storeLoaded$: BehaviorSubject = new BehaviorSubject(true); - const storeEntities$: BehaviorSubject = new BehaviorSubject( - mockStoreEntities - ); - - const mockSelect = ( - selector: MemoizedSelector - ) => { - switch (selector) { - case StoreFinderSelectors.getStoresLoading: - return () => storeLoading$.asObservable(); - case StoreFinderSelectors.getStoresSuccess: - return () => storeLoaded$.asObservable(); - case StoreFinderSelectors.getFindStoresEntities: - return () => storeEntities$.asObservable(); - default: - return () => EMPTY; - } - }; - beforeEach(() => { - spyOnProperty(NgrxStore, 'select').and.returnValue(mockSelect); + let mockSelectLoading: MemoizedSelector; + let mockSelectSuccess: MemoizedSelector; + let mockSelectEntities: MemoizedSelector< + StateWithStoreFinder, + FindStoresState + >; + beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot({}), - StoreModule.forFeature( - STORE_FINDER_FEATURE, - fromStoreReducers.getReducers() - ), - ], providers: [ StoreFinderService, { provide: WindowRef, useValue: MockWindowRef }, { provide: RoutingService, useClass: MockRoutingService }, GlobalMessageService, { provide: StoreFinderConfig, useClass: MockStoreFinderConfig }, + provideMockStore(), ], }); + store = TestBed.inject(MockStore); + + mockSelectLoading = store.overrideSelector( + StoreFinderSelectors.getStoresLoading as MemoizedSelector< + StateWithStoreFinder, + boolean + >, + true + ); + mockSelectSuccess = store.overrideSelector( + StoreFinderSelectors.getStoresSuccess as MemoizedSelector< + StateWithStoreFinder, + boolean + >, + true + ); + mockSelectEntities = store.overrideSelector( + StoreFinderSelectors.getFindStoresEntities as MemoizedSelector< + StateWithStoreFinder, + FindStoresState + >, + mockStoreEntities + ); + service = TestBed.inject(StoreFinderService); - store = TestBed.inject(Store); winRef = TestBed.inject(WindowRef); routingService = TestBed.inject(RoutingService); - spyOn(store, 'dispatch').and.callThrough(); - spyOn( - winRef.nativeWindow.navigator.geolocation, - 'watchPosition' - ).and.callThrough(); - spyOn( - winRef.nativeWindow.navigator.geolocation, - 'clearWatch' - ).and.callThrough(); - spyOn(routingService, 'getParams').and.returnValue(EMPTY); + vi.spyOn(store, 'dispatch'); + vi.spyOn(winRef.nativeWindow.navigator.geolocation, 'watchPosition'); + vi.spyOn(winRef.nativeWindow.navigator.geolocation, 'clearWatch'); + routerParam$.next({}); }); it('should inject StoreFinderService', inject( @@ -312,13 +306,18 @@ describe('StoreFinderService', () => { describe('Reload store entities on context change', () => { beforeEach(() => { - storeLoaded$.next(false); - storeLoading$.next(false); + mockSelectLoading.setResult(false); + mockSelectSuccess.setResult(false); + store.refreshState(); }); it('should dispatch findStores action on context change', () => { routerParam$.next({ country: 'US' }); - storeEntities$.next({ findStoresEntities: {}, findStoreEntityById: {} }); + mockSelectEntities.setResult({ + findStoresEntities: {}, + findStoreEntityById: {}, + }); + store.refreshState(); expect(store.dispatch).toHaveBeenCalledWith( new StoreFinderActions.FindStores({ queryText: '', @@ -334,7 +333,11 @@ describe('StoreFinderService', () => { it('should dispatch viewStoreById action on context change', () => { routerParam$.next({ store: storeId }); - storeEntities$.next({ findStoresEntities: {}, findStoreEntityById: {} }); + mockSelectEntities.setResult({ + findStoresEntities: {}, + findStoreEntityById: {}, + }); + store.refreshState(); expect(store.dispatch).toHaveBeenCalledWith( new StoreFinderActions.FindStoreById({ storeId }) ); diff --git a/feature-libs/storefinder/core/service/google-map-renderer.service.spec.ts b/feature-libs/storefinder/core/service/google-map-renderer.service.spec.ts index 331d081cf6e..b839a462c1a 100644 --- a/feature-libs/storefinder/core/service/google-map-renderer.service.spec.ts +++ b/feature-libs/storefinder/core/service/google-map-renderer.service.spec.ts @@ -1,7 +1,8 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { ScriptLoader } from '@spartacus/core'; +import { TestBed } from '@angular/core/testing'; +import { FeatureToggles, ScriptLoader } from '@spartacus/core'; // eslint-disable-next-line @nx/workspace-no-self-public-api-import -- ESLint is misfiring here: core and root are not the same library — they're separate entry points import { GOOGLE_MAPS_DEVELOPMENT_KEY_CONFIG } from '@spartacus/storefinder/root'; +import { vi } from 'vitest'; import { MockFeatureTogglesController, provideMockFeatureToggles, @@ -10,6 +11,7 @@ import { StoreFinderConfig } from '../config/store-finder-config'; import { StoreFinderService } from '../facade/store-finder.service'; import { GoogleMapRendererService } from './google-map-renderer.service'; import { StoreLocationService } from './store-location.service'; +import { Provider } from '@angular/core'; const MAP_DOM_ELEMENT_INNER_HTML = 'map dom element inner html'; const MOCK_MAPS_API_KEY = `mock-maps-api-key`; @@ -130,29 +132,37 @@ describe('GoogleMapRendererService', () => { let storeLocationServiceMock: StoreLocationService; let mapDomElement: HTMLElement; let config: StoreFinderConfig; - let featureToggles: MockFeatureTogglesController; - beforeEach(() => { + const featureToggles = { + useAdvancedGoogleMarkers: false, + }; + + const staticProviders: Provider[] = [ + GoogleMapRendererService, + { provide: ScriptLoader, useClass: ScriptLoaderMock }, + { + provide: StoreFinderService, + useClass: StoreFinderServiceMock, + }, + { + provide: StoreLocationService, + useClass: StoreLocationServiceMock, + }, + ]; + + const createTestBed = async (featureToggles: any) => { + vi.useFakeTimers(); advancedMarkerInstances = []; mapInstances = []; - const bed = TestBed.configureTestingModule({ + const bed = await TestBed.configureTestingModule({ providers: [ - GoogleMapRendererService, - { provide: ScriptLoader, useClass: ScriptLoaderMock }, - { - provide: StoreFinderService, - useClass: StoreFinderServiceMock, - }, - { - provide: StoreLocationService, - useClass: StoreLocationServiceMock, - }, + ...staticProviders, { provide: StoreFinderConfig, useValue: { googleMaps: { ...mockGoogleMapsConfig } }, }, - provideMockFeatureToggles({ useAdvancedGoogleMarkers: false }), + { provide: FeatureToggles, useValue: { ...featureToggles } }, ], }); @@ -162,16 +172,24 @@ describe('GoogleMapRendererService', () => { storeFinderServiceMock = bed.inject(StoreFinderService); storeLocationServiceMock = bed.inject(StoreLocationService); config = TestBed.inject(StoreFinderConfig); - featureToggles = TestBed.inject(MockFeatureTogglesController); + }; + + beforeEach(async () => { + TestBed.resetTestingModule(); + await createTestBed(featureToggles); + }); + + afterEach(() => { + vi.useRealTimers(); }); - it('should render map when an api key is provided in the config', fakeAsync(() => { + it('should render map when an api key is provided in the config', async () => { setApiKey(MOCK_MAPS_API_KEY); // given - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); - spyOn(storeFinderServiceMock, 'getStoreLatitude').and.callThrough(); - spyOn(storeFinderServiceMock, 'getStoreLongitude').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); + vi.spyOn(storeFinderServiceMock, 'getStoreLatitude'); + vi.spyOn(storeFinderServiceMock, 'getStoreLongitude'); // when googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); @@ -181,22 +199,22 @@ describe('GoogleMapRendererService', () => { src: config.googleMaps?.apiUrl, params: Object({ key: MOCK_MAPS_API_KEY }), attributes: { type: 'text/javascript' }, - callback: jasmine.any(Function) as any, + callback: expect.any(Function) as any, }); expect(storeFinderServiceMock.getStoreLatitude).toHaveBeenCalled(); expect(storeFinderServiceMock.getStoreLongitude).toHaveBeenCalled(); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(mapDomElement.innerHTML).toEqual(MAP_DOM_ELEMENT_INNER_HTML); - })); + }); - it('should render map when special "development" api key value is provided', fakeAsync(() => { + it('should render map when special "development" api key value is provided', async () => { setApiKey(GOOGLE_MAPS_DEVELOPMENT_KEY_CONFIG); // given - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); - spyOn(storeFinderServiceMock, 'getStoreLatitude').and.callThrough(); - spyOn(storeFinderServiceMock, 'getStoreLongitude').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); + vi.spyOn(storeFinderServiceMock, 'getStoreLatitude'); + vi.spyOn(storeFinderServiceMock, 'getStoreLongitude'); // when googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); @@ -206,36 +224,36 @@ describe('GoogleMapRendererService', () => { src: config.googleMaps?.apiUrl, params: Object({ key: '' }), attributes: { type: 'text/javascript' }, - callback: jasmine.any(Function) as any, + callback: expect.any(Function) as any, }); expect(storeFinderServiceMock.getStoreLatitude).toHaveBeenCalled(); expect(storeFinderServiceMock.getStoreLongitude).toHaveBeenCalled(); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(mapDomElement.innerHTML).toEqual(MAP_DOM_ELEMENT_INNER_HTML); - })); + }); - it('should not render map when no api key is provided (default config)', fakeAsync(() => { + it('should not render map when no api key is provided (default config)', () => { // given - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); // when googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); // then expect(scriptLoaderMock.embedScript).not.toHaveBeenCalled(); - })); + }); - it('should not create a new map if the map was already created', fakeAsync(() => { + it('should not create a new map if the map was already created', async () => { setApiKey(GOOGLE_MAPS_DEVELOPMENT_KEY_CONFIG); // given the map is already rendered googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); - tick(); + await vi.advanceTimersByTimeAsync(0); - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); - spyOn(storeFinderServiceMock, 'getStoreLatitude').and.callThrough(); - spyOn(storeFinderServiceMock, 'getStoreLongitude').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); + vi.spyOn(storeFinderServiceMock, 'getStoreLatitude'); + vi.spyOn(storeFinderServiceMock, 'getStoreLongitude'); // when rendering the map one more time googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); @@ -244,14 +262,14 @@ describe('GoogleMapRendererService', () => { expect(scriptLoaderMock.embedScript).toHaveBeenCalledTimes(0); expect(storeFinderServiceMock.getStoreLatitude).toHaveBeenCalled(); expect(storeFinderServiceMock.getStoreLongitude).toHaveBeenCalled(); - })); + }); - it('should embed the script with an empty src when apiUrl is not configured', fakeAsync(() => { + it('should embed the script with an empty src when apiUrl is not configured', () => { setApiKey(MOCK_MAPS_API_KEY); if (config.googleMaps) { config.googleMaps.apiUrl = undefined; } - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); @@ -259,28 +277,32 @@ describe('GoogleMapRendererService', () => { src: '', params: Object({ key: MOCK_MAPS_API_KEY }), attributes: { type: 'text/javascript' }, - callback: jasmine.any(Function) as any, + callback: expect.any(Function) as any, }); - })); + }); - it('should render map when selectMarkerHandler is not provided', fakeAsync(() => { + it('should render map when selectMarkerHandler is not provided', async () => { setApiKey(MOCK_MAPS_API_KEY); - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); googleMapRendererService.renderMap(mapDomElement, locations); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(mapDomElement.innerHTML).toEqual(MAP_DOM_ELEMENT_INNER_HTML); - })); + }); describe('with useGoogleMapsAsyncLoading enabled', () => { - beforeEach(() => { - featureToggles.set('useGoogleMapsAsyncLoading', true); + beforeEach(async () => { + TestBed.resetTestingModule(); + await createTestBed({ + ...featureToggles, + useGoogleMapsAsyncLoading: true, + }); setApiKey(MOCK_MAPS_API_KEY); }); - it('should embed the script with the loading=async and callback params', fakeAsync(() => { - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); + it('should embed the script with the loading=async and callback params', () => { + vi.spyOn(scriptLoaderMock, 'embedScript'); googleMapRendererService.renderMap( mapDomElement, @@ -293,23 +315,25 @@ describe('GoogleMapRendererService', () => { params: Object({ key: MOCK_MAPS_API_KEY, loading: 'async', - callback: jasmine.stringMatching( + callback: expect.stringMatching( /^__spartacusGoogleMapsInit_\d+$/ ) as any, }), attributes: { type: 'text/javascript' }, callback: undefined, }); - })); + }); - it('should draw the map from the global callback and clean it up', fakeAsync(() => { + it('should draw the map from the global callback and clean it up', async () => { let callbackName: string | undefined; - spyOn(scriptLoaderMock, 'embedScript').and.callFake((options: any) => { - callbackName = options.params?.callback; - (window as any)['google'] = createGoogleMock(); - // Emulate Google invoking the global callback once the API is ready. - (window as any)[callbackName as string](); - }); + vi.spyOn(scriptLoaderMock, 'embedScript').mockImplementation( + (options: any) => { + callbackName = options.params?.callback; + (window as any)['google'] = createGoogleMock(); + // Emulate Google invoking the global callback once the API is ready. + (window as any)[callbackName as string](); + } + ); googleMapRendererService.renderMap( mapDomElement, @@ -320,38 +344,42 @@ describe('GoogleMapRendererService', () => { expect(mapDomElement.innerHTML).toEqual(MAP_DOM_ELEMENT_INNER_HTML); // The global callback removes itself so it can't leak or fire twice. expect((window as any)[callbackName as string]).toBeUndefined(); - })); + }); }); describe('with useAdvancedGoogleMarkers enabled', () => { - beforeEach(() => { - featureToggles.set('useAdvancedGoogleMarkers', true); + beforeEach(async () => { + TestBed.resetTestingModule(); + await createTestBed({ + ...featureToggles, + useAdvancedGoogleMarkers: true, + }); setApiKey(MOCK_MAPS_API_KEY); }); - it('should create advanced markers from StoreLocationService coordinates', fakeAsync(() => { - spyOn(storeLocationServiceMock, 'getStoreLatitude').and.callThrough(); - spyOn(storeLocationServiceMock, 'getStoreLongitude').and.callThrough(); + it('should create advanced markers from StoreLocationService coordinates', async () => { + vi.spyOn(storeLocationServiceMock, 'getStoreLatitude'); + vi.spyOn(storeLocationServiceMock, 'getStoreLongitude'); googleMapRendererService.renderMap( mapDomElement, locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(storeLocationServiceMock.getStoreLatitude).toHaveBeenCalled(); expect(storeLocationServiceMock.getStoreLongitude).toHaveBeenCalled(); expect(advancedMarkerInstances.length).toBe(locations.length); expect(advancedMarkerInstances[0].position.lat).toBe(30); expect(advancedMarkerInstances[0].position.lng).toBe(40); - })); + }); - it('should skip markers when StoreLocationService returns undefined coordinates', fakeAsync(() => { - spyOn(storeLocationServiceMock, 'getStoreLatitude').and.returnValue( + it('should skip markers when StoreLocationService returns undefined coordinates', async () => { + vi.spyOn(storeLocationServiceMock, 'getStoreLatitude').mockReturnValue( undefined ); - spyOn(storeLocationServiceMock, 'getStoreLongitude').and.returnValue( + vi.spyOn(storeLocationServiceMock, 'getStoreLongitude').mockReturnValue( undefined ); @@ -360,16 +388,16 @@ describe('GoogleMapRendererService', () => { locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(advancedMarkerInstances.length).toBe(0); - })); + }); - it('should create the map with an undefined center when coordinates are missing', fakeAsync(() => { - spyOn(storeLocationServiceMock, 'getStoreLatitude').and.returnValue( + it('should create the map with an undefined center when coordinates are missing', async () => { + vi.spyOn(storeLocationServiceMock, 'getStoreLatitude').mockReturnValue( undefined ); - spyOn(storeLocationServiceMock, 'getStoreLongitude').and.returnValue( + vi.spyOn(storeLocationServiceMock, 'getStoreLongitude').mockReturnValue( undefined ); @@ -378,32 +406,32 @@ describe('GoogleMapRendererService', () => { locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(mapInstances[0].mapProp.center).toBeUndefined(); - })); + }); - it('should render a numbered pin inside the marker content wrapper', fakeAsync(() => { + it('should render a numbered pin inside the marker content wrapper', async () => { googleMapRendererService.renderMap( mapDomElement, locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); // The content is a wrapper the service owns; the PinElement (with the // store number as its glyph) is nested inside it. const content = advancedMarkerInstances[0].content as HTMLElement; expect(content.textContent).toBe('1'); - })); + }); - it('should toggle the bounce class on the inner pin on mouseover and mouseout', fakeAsync(() => { + it('should toggle the bounce class on the inner pin on mouseover and mouseout', async () => { googleMapRendererService.renderMap( mapDomElement, locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); // Hover events are handled on the transformed content wrapper, but the // bounce class must land on the inner pin so it doesn't fight Google's @@ -417,29 +445,29 @@ describe('GoogleMapRendererService', () => { wrapper.dispatchEvent(new Event('mouseout')); expect(pin.classList.contains('cx-store-marker-bounce')).toBe(false); - })); + }); - it('should mark the marker clickable and invoke selectMarkerHandler with the marker index on gmp-click', fakeAsync(() => { - const handler = jasmine.createSpy('selectMarkerHandler'); + it('should mark the marker clickable and invoke selectMarkerHandler with the marker index on gmp-click', async () => { + const handler = vi.fn(); googleMapRendererService.renderMap(mapDomElement, locations, handler); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(advancedMarkerInstances[0].gmpClickable).toBe(true); advancedMarkerInstances[0].listeners['gmp-click'](); expect(handler).toHaveBeenCalledWith(0); - })); + }); - it('should not register a click listener nor mark clickable when selectMarkerHandler is not provided', fakeAsync(() => { + it('should not register a click listener nor mark clickable when selectMarkerHandler is not provided', async () => { googleMapRendererService.renderMap(mapDomElement, locations); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(advancedMarkerInstances[0].gmpClickable).toBe(false); expect(advancedMarkerInstances[0].listeners['gmp-click']).toBeUndefined(); - })); + }); - it('should load the marker library when embedding the script', fakeAsync(() => { - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); + it('should load the marker library when embedding the script', async () => { + vi.spyOn(scriptLoaderMock, 'embedScript'); googleMapRendererService.renderMap( mapDomElement, @@ -454,73 +482,73 @@ describe('GoogleMapRendererService', () => { libraries: 'marker', }), attributes: { type: 'text/javascript' }, - callback: jasmine.any(Function) as any, + callback: expect.any(Function) as any, }); - })); + }); - it('should create the map with the configured mapId', fakeAsync(() => { + it('should create the map with the configured mapId', async () => { googleMapRendererService.renderMap( mapDomElement, locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(mapInstances[0].mapProp.mapId).toBe(mockGoogleMapsConfig.mapId); - })); + }); }); - it('should not load the marker library nor set a mapId when the toggle is disabled', fakeAsync(() => { + it('should not load the marker library nor set a mapId when the toggle is disabled', async () => { setApiKey(MOCK_MAPS_API_KEY); - spyOn(scriptLoaderMock, 'embedScript').and.callThrough(); + vi.spyOn(scriptLoaderMock, 'embedScript'); googleMapRendererService.renderMap(mapDomElement, locations, selectedIndex); - tick(); + await vi.advanceTimersByTimeAsync(0); expect(scriptLoaderMock.embedScript).toHaveBeenCalledWith({ src: config.googleMaps?.apiUrl, params: Object({ key: MOCK_MAPS_API_KEY }), attributes: { type: 'text/javascript' }, - callback: jasmine.any(Function) as any, + callback: expect.any(Function) as any, }); expect(mapInstances[0].mapProp.mapId).toBeUndefined(); - })); + }); describe('centerMap', () => { - function renderAndGetMap(): any { + async function renderAndGetMap(): Promise { setApiKey(MOCK_MAPS_API_KEY); googleMapRendererService.renderMap( mapDomElement, locations, selectedIndex ); - tick(); + await vi.advanceTimersByTimeAsync(0); return mapInstances[0]; } - it('should pan the map to the given coordinates', fakeAsync(() => { - const map = renderAndGetMap(); - spyOn(map, 'panTo'); + it('should pan the map to the given coordinates', async () => { + const map = await renderAndGetMap(); + vi.spyOn(map, 'panTo'); googleMapRendererService.centerMap(30, 40); expect(map.panTo).toHaveBeenCalledWith({ lat: 30, lng: 40 }); - })); + }); - it('should zoom to selectedMarkerScale when configured', fakeAsync(() => { - const map = renderAndGetMap(); - spyOn(map, 'setZoom'); + it('should zoom to selectedMarkerScale when configured', async () => { + const map = await renderAndGetMap(); + vi.spyOn(map, 'setZoom'); googleMapRendererService.centerMap(30, 40); expect(map.setZoom).toHaveBeenCalledWith( mockGoogleMapsConfig.selectedMarkerScale ); - })); + }); - it('should not zoom when selectedMarkerScale is not configured', fakeAsync(() => { - const map = renderAndGetMap(); - spyOn(map, 'setZoom'); + it('should not zoom when selectedMarkerScale is not configured', async () => { + const map = await renderAndGetMap(); + vi.spyOn(map, 'setZoom'); if (config.googleMaps) { config.googleMaps.selectedMarkerScale = undefined; } @@ -528,7 +556,7 @@ describe('GoogleMapRendererService', () => { googleMapRendererService.centerMap(30, 40); expect(map.setZoom).not.toHaveBeenCalled(); - })); + }); it('should do nothing when no map has been rendered', () => { expect(() => googleMapRendererService.centerMap(30, 40)).not.toThrow(); @@ -539,7 +567,7 @@ describe('GoogleMapRendererService', () => { if (config.googleMaps) { config.googleMaps.apiKey = keyValue; } else { - fail('Config undefined'); + throw new Error('Config undefined'); } } }); diff --git a/feature-libs/storefinder/core/store/effects/find-stores.effect.spec.ts b/feature-libs/storefinder/core/store/effects/find-stores.effect.spec.ts index 1a5e9d64644..fb42240b203 100644 --- a/feature-libs/storefinder/core/store/effects/find-stores.effect.spec.ts +++ b/feature-libs/storefinder/core/store/effects/find-stores.effect.spec.ts @@ -3,10 +3,10 @@ import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { cold, hot } from 'jasmine-marbles'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderConnector } from '../../connectors/store-finder.connector'; import { StoreFinderActions } from '../actions/index'; import * as fromEffects from './find-stores.effect'; -import createSpy = jasmine.createSpy; import { GeoPoint, SearchConfig } from '@spartacus/core'; import { provideHttpClient, @@ -17,8 +17,8 @@ const singleStoreResult = {}; const searchResult: any = { stores: [] }; const mockStoreFinderConnector = { - get: createSpy('connector.get').and.returnValue(of(singleStoreResult)), - search: createSpy('connector.search').and.returnValue(of(searchResult)), + get: vi.fn().mockReturnValue(of(singleStoreResult)), + search: vi.fn().mockReturnValue(of(searchResult)), }; describe('FindStores Effects', () => { diff --git a/feature-libs/storefinder/core/store/effects/view-all-stores.effect.spec.ts b/feature-libs/storefinder/core/store/effects/view-all-stores.effect.spec.ts index 00074f096ac..ce551960941 100644 --- a/feature-libs/storefinder/core/store/effects/view-all-stores.effect.spec.ts +++ b/feature-libs/storefinder/core/store/effects/view-all-stores.effect.spec.ts @@ -3,10 +3,10 @@ import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { cold, hot } from 'jasmine-marbles'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { StoreFinderConnector } from '../../connectors/store-finder.connector'; import { StoreFinderActions } from '../actions/index'; import * as fromEffects from './view-all-stores.effect'; -import createSpy = jasmine.createSpy; import { OccConfig, SiteContextActions } from '@spartacus/core'; import { StoreCount } from '../../model/store-finder.model'; import { @@ -29,9 +29,7 @@ const storesCountResult: StoreCount[] = [ ]; const mockStoreFinderConnector = { - getCounts: createSpy('connector.getCounts').and.returnValue( - of(storesCountResult) - ), + getCounts: vi.fn().mockReturnValue(of(storesCountResult)), }; describe('ViewAllStores Effects', () => { diff --git a/feature-libs/storefinder/core/store/selectors/find-stores.selectors.spec.ts b/feature-libs/storefinder/core/store/selectors/find-stores.selectors.spec.ts index f6e69b48f6c..a2f17050ba8 100644 --- a/feature-libs/storefinder/core/store/selectors/find-stores.selectors.spec.ts +++ b/feature-libs/storefinder/core/store/selectors/find-stores.selectors.spec.ts @@ -1,5 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; +import { vi } from 'vitest'; import { StoreFinderActions } from '../actions/index'; import * as fromReducers from '../reducers/index'; import { StoreFinderSelectors } from '../selectors/index'; @@ -25,7 +26,7 @@ describe('FindStores Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('findStores', () => { diff --git a/feature-libs/storefinder/core/store/selectors/view-all-stores.selectors.spec.ts b/feature-libs/storefinder/core/store/selectors/view-all-stores.selectors.spec.ts index 6846447f2fc..b2859ac3242 100644 --- a/feature-libs/storefinder/core/store/selectors/view-all-stores.selectors.spec.ts +++ b/feature-libs/storefinder/core/store/selectors/view-all-stores.selectors.spec.ts @@ -1,5 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { select, Store, StoreModule } from '@ngrx/store'; +import { vi } from 'vitest'; import { StoreFinderActions } from '../actions/index'; import * as fromReducers from '../reducers/index'; import { StoreFinderSelectors } from '../selectors/index'; @@ -25,7 +26,7 @@ describe('ViewAllStores Selectors', () => { }); store = TestBed.inject(Store); - spyOn(store, 'dispatch').and.callThrough(); + vi.spyOn(store, 'dispatch'); }); describe('viewAllStores', () => { diff --git a/feature-libs/storefinder/karma.conf.js b/feature-libs/storefinder/karma.conf.js deleted file mode 100644 index c07b43a87bf..00000000000 --- a/feature-libs/storefinder/karma.conf.js +++ /dev/null @@ -1,52 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-storefinder.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/storefinder'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 85, - lines: 85, - branches: 70, - functions: 80, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/storefinder/occ/adapters/occ-store-finder.adapter.spec.ts b/feature-libs/storefinder/occ/adapters/occ-store-finder.adapter.spec.ts index 28ac9c03aaa..b64e4e0b272 100644 --- a/feature-libs/storefinder/occ/adapters/occ-store-finder.adapter.spec.ts +++ b/feature-libs/storefinder/occ/adapters/occ-store-finder.adapter.spec.ts @@ -3,6 +3,7 @@ import { provideHttpClientTesting, } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; import { OccStoreFinderAdapter } from './occ-store-finder.adapter'; import { BaseOccUrlProperties, @@ -75,9 +76,9 @@ describe('OccStoreFinderAdapter', () => { httpMock = TestBed.inject(HttpTestingController); converterService = TestBed.inject(ConverterService); occEndpointsService = TestBed.inject(OccEndpointsService); - spyOn(converterService, 'pipeable').and.callThrough(); - spyOn(converterService, 'pipeableMany').and.callThrough(); - spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converterService, 'pipeable'); + vi.spyOn(converterService, 'pipeableMany'); + vi.spyOn(occEndpointsService, 'buildUrl'); }); afterEach(() => { diff --git a/feature-libs/storefinder/project.json b/feature-libs/storefinder/project.json index c10019b7c91..6c764de23e4 100644 --- a/feature-libs/storefinder/project.json +++ b/feature-libs/storefinder/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/storefinder/test.ts", - "tsConfig": "feature-libs/storefinder/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/storefinder/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/storefinder/test.ts b/feature-libs/storefinder/test.ts deleted file mode 100644 index 381a72c5ff2..00000000000 --- a/feature-libs/storefinder/test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -// Patching Object.defineProperty unlocks frozen JS symbols and makes possible to mock them. -// Should be used with caution, and only if there is no other way to mock stuff (eg. by DI) -// Has to be imported just after zone.js imports. -import 'testing/patch-object-define-property'; - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/storefinder/tsconfig.spec.json b/feature-libs/storefinder/tsconfig.spec.json index 34d8415e3a6..d52c68cbde6 100644 --- a/feature-libs/storefinder/tsconfig.spec.json +++ b/feature-libs/storefinder/tsconfig.spec.json @@ -4,9 +4,16 @@ "outDir": "../../out-tsc/spec", "module": "preserve", "strict": false, - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/storefinder/vitest.config.ts b/feature-libs/storefinder/vitest.config.ts new file mode 100644 index 00000000000..9f2f5d24f98 --- /dev/null +++ b/feature-libs/storefinder/vitest.config.ts @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + 'core-libs/core/src/features-config/feature-toggles/testing': `${root}/core-libs/core/src/features-config/feature-toggles/testing/index.ts`, + }, + }, + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/storefinder`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-storefinder.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal-component.service.spec.ts b/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal-component.service.spec.ts index 3dfd0183177..1c59bcc3286 100644 --- a/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal-component.service.spec.ts +++ b/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal-component.service.spec.ts @@ -5,19 +5,18 @@ import { EventService, } from '@spartacus/core'; import { GetSubscriptionByCodeReloadEvent } from '@spartacus/subscription-billing/root'; -import { throwError } from 'rxjs'; +import { firstValueFrom, throwError } from 'rxjs'; +import { vi } from 'vitest'; import { SubscriptionActionsModalComponentService } from './subscription-actions-modal-component.service'; describe('SubscriptionActionsModalComponentService', () => { let service: SubscriptionActionsModalComponentService; - let globalMessageService: jasmine.SpyObj; - let eventService: jasmine.SpyObj; + let globalMessageService: any; + let eventService: any; beforeEach(() => { - const globalMessageSpy = jasmine.createSpyObj('GlobalMessageService', [ - 'add', - ]); - const eventServiceSpy = jasmine.createSpyObj('EventService', ['dispatch']); + const globalMessageSpy = { add: vi.fn() }; + const eventServiceSpy = { dispatch: vi.fn() }; TestBed.configureTestingModule({ providers: [ @@ -28,52 +27,44 @@ describe('SubscriptionActionsModalComponentService', () => { }); service = TestBed.inject(SubscriptionActionsModalComponentService); - globalMessageService = TestBed.inject( - GlobalMessageService - ) as jasmine.SpyObj; - eventService = TestBed.inject(EventService) as jasmine.SpyObj; + globalMessageService = TestBed.inject(GlobalMessageService) as any; + eventService = TestBed.inject(EventService) as any; }); describe('handleError', () => { - it('should call onDialogClose with "error" and show global error message', (done) => { - const onDialogClose = jasmine.createSpy('onDialogClose'); + it('should call onDialogClose with "error" and show global error message', async () => { + const onDialogClose = vi.fn(); const errorHandler = service.handleError(onDialogClose, 'test.error'); - throwError(() => new Error('Test')) - .pipe(errorHandler) - .subscribe({ - complete: () => { - expect(onDialogClose).toHaveBeenCalledWith('error'); - expect(globalMessageService.add).toHaveBeenCalledWith( - { key: 'test.error' }, - GlobalMessageType.MSG_TYPE_ERROR - ); - done(); - }, - }); + await firstValueFrom( + throwError(() => new Error('Test')).pipe(errorHandler) + ).catch(() => {}); + + expect(onDialogClose).toHaveBeenCalledWith('error'); + expect(globalMessageService.add).toHaveBeenCalledWith( + { key: 'test.error' }, + GlobalMessageType.MSG_TYPE_ERROR + ); }); - it('should default to unknown error key if none provided', (done) => { + it('should default to unknown error key if none provided', async () => { const errorHandler = service.handleError(); - throwError(() => new Error('Test')) - .pipe(errorHandler) - .subscribe({ - complete: () => { - expect(globalMessageService.add).toHaveBeenCalledWith( - { key: 'subscriptionActions.unknownError' }, - GlobalMessageType.MSG_TYPE_ERROR - ); - done(); - }, - }); + await firstValueFrom( + throwError(() => new Error('Test')).pipe(errorHandler) + ).catch(() => {}); + + expect(globalMessageService.add).toHaveBeenCalledWith( + { key: 'subscriptionActions.unknownError' }, + GlobalMessageType.MSG_TYPE_ERROR + ); }); }); describe('handleSuccess', () => { it('should call onDialogClose with "Success", dispatch event, and show success message', () => { - const onDialogClose = jasmine.createSpy('onDialogClose'); + const onDialogClose = vi.fn(); const observer = service.handleSuccess( 'test.success', onDialogClose, diff --git a/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal.component.spec.ts b/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal.component.spec.ts index 5c3f043f5aa..51f0ed7f353 100644 --- a/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal.component.spec.ts +++ b/feature-libs/subscription-billing/components/actions-modal/subscription-actions-modal.component.spec.ts @@ -1,9 +1,4 @@ -import { - ComponentFixture, - TestBed, - fakeAsync, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SubscriptionActionsModalComponent } from './subscription-actions-modal.component'; import { Observable, of, throwError } from 'rxjs'; import { @@ -22,6 +17,7 @@ import { LaunchDialogService } from '@spartacus/storefront'; import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { signal } from '@angular/core'; +import { vi } from 'vitest'; describe('SubscriptionActionsModalComponent', () => { let component: SubscriptionActionsModalComponent; @@ -37,63 +33,50 @@ describe('SubscriptionActionsModalComponent', () => { } } const mockRoutingService = { - go: jasmine.createSpy('go'), + go: vi.fn(), }; class MockLanguageService { getActive(): Observable { return of('en'); } } - let mockCancelFacade: jasmine.SpyObj; - let mockGlobalMessageService: jasmine.SpyObj; - let mockLaunchDialogService: jasmine.SpyObj; - let mockEventService: jasmine.SpyObj; + let mockCancelFacade: any; + let mockGlobalMessageService: any; + let mockLaunchDialogService: any; + let mockEventService: any; beforeEach(async () => { - mockCancelFacade = jasmine.createSpyObj('SubscriptionActionsFacade', [ - 'getEffectiveCancellationDate', - 'cancelSubscription', - 'withdrawSubscription', - 'reverseCancellation', - 'extendSubscription', - 'getExtensionEffectiveDate', - ]); - - mockCancelFacade.getEffectiveCancellationDate.and.returnValue( + mockCancelFacade = { + getEffectiveCancellationDate: vi.fn(), + cancelSubscription: vi.fn(), + withdrawSubscription: vi.fn(), + reverseCancellation: vi.fn(), + extendSubscription: vi.fn(), + getExtensionEffectiveDate: vi.fn(), + }; + + mockCancelFacade.getEffectiveCancellationDate.mockReturnValue( of({ subscriptionEndAt: '2025-12-31' }) ); - mockCancelFacade.cancelSubscription.and.returnValue(of({})); - mockCancelFacade.withdrawSubscription.and.returnValue(of({})); - mockCancelFacade.reverseCancellation.and.returnValue(of({})); - mockCancelFacade.extendSubscription.and.returnValue(of({})); - mockCancelFacade.getExtensionEffectiveDate.and.returnValue( + mockCancelFacade.cancelSubscription.mockReturnValue(of({})); + mockCancelFacade.withdrawSubscription.mockReturnValue(of({})); + mockCancelFacade.reverseCancellation.mockReturnValue(of({})); + mockCancelFacade.extendSubscription.mockReturnValue(of({})); + mockCancelFacade.getExtensionEffectiveDate.mockReturnValue( of({ subscriptionEndAt: '2024-12-31' }) ); - mockGlobalMessageService = jasmine.createSpyObj('GlobalMessageService', [ - 'add', - ]); - - mockLaunchDialogService = jasmine.createSpyObj( - 'LaunchDialogService', - ['closeDialog'], - { - data$: of({ id: 'subId', code: 'ABC123', mode: 'cancel' }), - } - ); + mockGlobalMessageService = { add: vi.fn() }; - mockLaunchDialogService = jasmine.createSpyObj( - 'LaunchDialogService', - ['closeDialog'], - { - data$: of({ - code: 'ABC123', - id: 'subId', - mode: 'cancel', - }), - } - ); + mockLaunchDialogService = { + closeDialog: vi.fn(), + data$: of({ + code: 'ABC123', + id: 'subId', + mode: 'cancel', + }), + }; - mockEventService = jasmine.createSpyObj('EventService', ['dispatch']); + mockEventService = { dispatch: vi.fn() }; await TestBed.configureTestingModule({ imports: [RouterTestingModule, SubscriptionActionsModalComponent], @@ -131,7 +114,7 @@ describe('SubscriptionActionsModalComponent', () => { describe('onConfirm', () => { it('should confirm cancel subscription successfully', () => { - mockCancelFacade.cancelSubscription.and.returnValue(of({})); + mockCancelFacade.cancelSubscription.mockReturnValue(of({})); component.onConfirm(); @@ -142,7 +125,7 @@ describe('SubscriptionActionsModalComponent', () => { }); it('should confirm extend subscription successfully', () => { - mockCancelFacade.extendSubscription.and.returnValue(of({})); + mockCancelFacade.extendSubscription.mockReturnValue(of({})); component.mode = signal('extend'); component.onConfirm(); @@ -152,35 +135,35 @@ describe('SubscriptionActionsModalComponent', () => { ); }); - it('should handle cancel subscription API error', fakeAsync(() => { - mockCancelFacade.cancelSubscription.and.returnValue( + it('should handle cancel subscription API error', async () => { + mockCancelFacade.cancelSubscription.mockReturnValue( throwError(() => new Error('Cancel Error')) ); component.onConfirm(); - tick(); + await Promise.resolve(); expect(mockGlobalMessageService.add).toHaveBeenCalledWith( { key: 'subscriptionActions.unknownError' }, GlobalMessageType.MSG_TYPE_ERROR ); expect(mockLaunchDialogService.closeDialog).toHaveBeenCalledWith('error'); - })); + }); - it('should handle extend subscription API error', fakeAsync(() => { - mockCancelFacade.extendSubscription.and.returnValue( + it('should handle extend subscription API error', async () => { + mockCancelFacade.extendSubscription.mockReturnValue( throwError(() => new Error('Extend Error')) ); component.mode = signal('extend'); component.onConfirm(); - tick(); + await Promise.resolve(); expect(mockGlobalMessageService.add).toHaveBeenCalledWith( { key: 'subscriptionActions.unknownError' }, GlobalMessageType.MSG_TYPE_ERROR ); expect(mockLaunchDialogService.closeDialog).toHaveBeenCalledWith('error'); - })); + }); it('should confirm withdrawal successfully', () => { - mockCancelFacade.withdrawSubscription.and.returnValue(of({})); + mockCancelFacade.withdrawSubscription.mockReturnValue(of({})); (component as any).subscriptionDetailSignal.set({ id: 'subId', code: 'ABC123', @@ -195,8 +178,8 @@ describe('SubscriptionActionsModalComponent', () => { ); }); - it('should handle withdraw API error', fakeAsync(() => { - mockCancelFacade.withdrawSubscription.and.returnValue( + it('should handle withdraw API error', async () => { + mockCancelFacade.withdrawSubscription.mockReturnValue( throwError(() => new Error('Error')) ); @@ -207,17 +190,17 @@ describe('SubscriptionActionsModalComponent', () => { }); component.onConfirm(); - tick(); + await Promise.resolve(); expect(mockLaunchDialogService.closeDialog).toHaveBeenCalledWith('error'); expect(mockGlobalMessageService.add).toHaveBeenCalledWith( { key: 'subscriptionActions.unknownError' }, GlobalMessageType.MSG_TYPE_ERROR ); - })); + }); it('should confirm resubscribe successfully', () => { - mockCancelFacade.reverseCancellation.and.returnValue(of({})); + mockCancelFacade.reverseCancellation.mockReturnValue(of({})); (component as any).subscriptionDetailSignal.set({ id: 'subId', @@ -252,24 +235,24 @@ describe('SubscriptionActionsModalComponent', () => { ); }); - it('should handle error from getEffectiveCancellationDate in effect', fakeAsync(() => { - mockCancelFacade.getEffectiveCancellationDate.and.returnValue( + it('should handle error from getEffectiveCancellationDate in effect', async () => { + mockCancelFacade.getEffectiveCancellationDate.mockReturnValue( throwError(() => new Error('Load Cancel Data Error')) ); fixture = TestBed.createComponent(SubscriptionActionsModalComponent); component = fixture.componentInstance; fixture.detectChanges(); - tick(); + await Promise.resolve(); expect(mockGlobalMessageService.add).toHaveBeenCalledWith( { key: 'subscriptionActions.unknownError' }, GlobalMessageType.MSG_TYPE_ERROR ); - })); + }); - it('should handle error from getExtendEffectiveDate in effect', fakeAsync(() => { - mockCancelFacade.getExtensionEffectiveDate.and.returnValue( + it('should handle error from getExtendEffectiveDate in effect', async () => { + mockCancelFacade.getExtensionEffectiveDate.mockReturnValue( throwError(() => new Error('Load Cancel Data Error')) ); @@ -277,12 +260,12 @@ describe('SubscriptionActionsModalComponent', () => { component = fixture.componentInstance; component.getExtensionEffectiveDate(); fixture.detectChanges(); - tick(); + await Promise.resolve(); expect(mockGlobalMessageService.add).toHaveBeenCalledWith( { key: 'subscriptionActions.unknownError' }, GlobalMessageType.MSG_TYPE_ERROR ); - })); + }); }); }); diff --git a/feature-libs/subscription-billing/components/billing-details/subscription-billing-details.component.spec.ts b/feature-libs/subscription-billing/components/billing-details/subscription-billing-details.component.spec.ts index 8072f1872ff..87a2c4c45e0 100644 --- a/feature-libs/subscription-billing/components/billing-details/subscription-billing-details.component.spec.ts +++ b/feature-libs/subscription-billing/components/billing-details/subscription-billing-details.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { vi } from 'vitest'; import { SubscriptionBillingDetailsComponent } from './subscription-billing-details.component'; import { EventService, @@ -109,8 +109,8 @@ describe('SubscriptionBillingDetailsComponent', () => { eventService = TestBed.inject(EventService); facade = TestBed.inject(SubscriptionBillingFacade); - spyOn(eventService, 'dispatch').and.callThrough(); - spyOn(facade, 'getSubscriptionBillByCode').and.callThrough(); + vi.spyOn(eventService, 'dispatch'); + vi.spyOn(facade, 'getSubscriptionBillByCode'); routerParam$.next({ ticketCode: 's1' }); fixture = TestBed.createComponent(SubscriptionBillingDetailsComponent); component = fixture.componentInstance; diff --git a/feature-libs/subscription-billing/components/details/subscription-details.component.spec.ts b/feature-libs/subscription-billing/components/details/subscription-details.component.spec.ts index 3b205bb6c1f..8199db84c6f 100644 --- a/feature-libs/subscription-billing/components/details/subscription-details.component.spec.ts +++ b/feature-libs/subscription-billing/components/details/subscription-details.component.spec.ts @@ -17,6 +17,7 @@ import { SubscriptionFacade, } from '@spartacus/subscription-billing/root'; import { BehaviorSubject, Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { SubscriptionDetailsComponent } from './subscription-details.component'; const routerParam$: BehaviorSubject<{ [key: string]: string; @@ -88,7 +89,7 @@ describe('SubscriptionDetailsComponent', () => { .compileComponents(); eventService = TestBed.inject(EventService); facade = TestBed.inject(SubscriptionFacade); - spyOn(eventService, 'dispatch').and.callThrough(); + vi.spyOn(eventService, 'dispatch'); routerParam$.next({ ticketCode: 's1' }); fixture = TestBed.createComponent(SubscriptionDetailsComponent); component = fixture.componentInstance; @@ -99,7 +100,7 @@ describe('SubscriptionDetailsComponent', () => { expect(component).toBeTruthy(); }); it('should reload data', () => { - spyOn(facade, 'getSubscriptionByCode').and.callThrough(); + vi.spyOn(facade, 'getSubscriptionByCode'); component.ngOnInit(); expect(eventService.dispatch).toHaveBeenCalled(); expect(facade.getSubscriptionByCode).toHaveBeenCalled(); @@ -113,7 +114,10 @@ describe('SubscriptionDetailsComponent', () => { const mode = 'cancel'; (component as any).subscriptionDetails$ = of(subscription); - const openDialogSpy = spyOn(launchDialogService, 'openDialogAndSubscribe'); + const openDialogSpy = vi.spyOn( + launchDialogService, + 'openDialogAndSubscribe' + ); component.showSubscriptionActionsDialog(mode); diff --git a/feature-libs/subscription-billing/components/list/billing/subscription-billing-list.component.spec.ts b/feature-libs/subscription-billing/components/list/billing/subscription-billing-list.component.spec.ts index 49b978ad2ff..b81420c45d4 100644 --- a/feature-libs/subscription-billing/components/list/billing/subscription-billing-list.component.spec.ts +++ b/feature-libs/subscription-billing/components/list/billing/subscription-billing-list.component.spec.ts @@ -15,6 +15,7 @@ import { By } from '@angular/platform-browser'; import { SubscriptionBillingListComponent } from '@spartacus/subscription-billing/components'; import { ActivatedRoute } from '@angular/router'; import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; +import { vi } from 'vitest'; const listWithData: SubscriptionBillsList = { pagination: { @@ -262,7 +263,7 @@ describe('SubscriptionBillingListComponent', () => { }); it('should set the sort order correctly', () => { - spyOn(facadeSpy, 'getSubscriptionBillsList').and.returnValue( + vi.spyOn(facadeSpy, 'getSubscriptionBillsList').mockReturnValue( of(listWithData) ); component.onSortCodeChange('byDocumentNumberAsc'); @@ -275,15 +276,14 @@ describe('SubscriptionBillingListComponent', () => { }); it('should set the date filter correctly', () => { + fixture.detectChanges(); // initialize component so async pipe stabilizes component.billsDateFilterForm.controls.from.setValue('2026-01-31'); component.billsDateFilterForm.controls.to.setValue('2026-12-31'); component.onFilterDateChange(); - fixture.detectChanges(); expect(component.minDate).toEqual('2026-01-31'); expect(component.maxDate).toEqual('2026-12-31'); component.onDateFilterSubmit(); - fixture.detectChanges(); expect(component.listParams).toEqual({ pageNumber: 0, sortCode: undefined, @@ -291,7 +291,6 @@ describe('SubscriptionBillingListComponent', () => { }); component.onResetFilterDate(); - fixture.detectChanges(); expect(component.minDate).toBeNull(); expect(component.maxDate).toBeNull(); expect(component.listParams).toEqual({ @@ -303,7 +302,6 @@ describe('SubscriptionBillingListComponent', () => { component.minDate = '2026-12-31'; component.maxDate = '2026-12-31'; component.onResetFilterDate(); - fixture.detectChanges(); expect(component.minDate).toBeNull(); expect(component.maxDate).toBeNull(); expect(component.listParams).toEqual({ @@ -313,7 +311,6 @@ describe('SubscriptionBillingListComponent', () => { }); component.onResetDateRange(); - fixture.detectChanges(); expect(component.minDate).toBeNull(); expect(component.maxDate).toBeNull(); expect(component.listParams).toEqual({ @@ -324,7 +321,6 @@ describe('SubscriptionBillingListComponent', () => { component.maxDate = '2026-12-31'; component.onResetDateRange(); - fixture.detectChanges(); expect(component.minDate).toBeNull(); expect(component.maxDate).toBeNull(); expect(component.listParams).toEqual({ diff --git a/feature-libs/subscription-billing/components/list/subscription-list.component.spec.ts b/feature-libs/subscription-billing/components/list/subscription-list.component.spec.ts index d5205926ec3..9a9fc60b642 100644 --- a/feature-libs/subscription-billing/components/list/subscription-list.component.spec.ts +++ b/feature-libs/subscription-billing/components/list/subscription-list.component.spec.ts @@ -1,10 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { - ComponentFixture, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { @@ -21,6 +16,7 @@ import { SubscriptionList, } from '@spartacus/subscription-billing/root'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { SubscriptionListComponent } from './subscription-list.component'; const listWithData: SubscriptionList = { @@ -127,7 +123,7 @@ describe('SubscriptionListComponent', () => { expect(component).toBeTruthy(); }); it('should show list with pagination and sort if data is present', () => { - spyOn(facade, 'getSubscriptionList').and.returnValue(of(listWithData)); + vi.spyOn(facade, 'getSubscriptionList').mockReturnValue(of(listWithData)); fixture.detectChanges(); expect( fixture.debugElement.queryAll(By.css('.subscription-list-sort.top')) @@ -142,7 +138,7 @@ describe('SubscriptionListComponent', () => { ).toEqual(2); }); it('should show no subscription is data is not present', () => { - spyOn(facade, 'getSubscriptionList').and.returnValue(of(listWithNoData)); + vi.spyOn(facade, 'getSubscriptionList').mockReturnValue(of(listWithNoData)); fixture.detectChanges(); expect( fixture.debugElement.queryAll(By.css('.subscription-list-sort.top')) @@ -156,25 +152,27 @@ describe('SubscriptionListComponent', () => { fixture.debugElement.queryAll(By.css('.subscription')).length ).toEqual(0); }); - it('should set the sort order correctly', fakeAsync(() => { - spyOn(facade, 'getSubscriptionList').and.returnValue(of(listWithData)); + it('should set the sort order correctly', async () => { + vi.spyOn(facade, 'getSubscriptionList').mockReturnValue(of(listWithData)); component.changeSortCode('byDocumentNumberAsc'); - tick(); + await Promise.resolve(); fixture.detectChanges(); - const lastCallArgs = ( - facade.getSubscriptionList as jasmine.Spy - ).calls.mostRecent().args; - expect(lastCallArgs).toEqual([5, 0, 'byDocumentNumberAsc']); - })); + expect(facade.getSubscriptionList).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + 'byDocumentNumberAsc' + ); + }); - it('should set the sort order correctly', fakeAsync(() => { - spyOn(facade, 'getSubscriptionList').and.returnValue(of(listWithData)); + it('should set the page number correctly', async () => { + vi.spyOn(facade, 'getSubscriptionList').mockReturnValue(of(listWithData)); component.pageChange(2); - tick(); + await Promise.resolve(); fixture.detectChanges(); - const lastCallArgs = ( - facade.getSubscriptionList as jasmine.Spy - ).calls.mostRecent().args; - expect(lastCallArgs).toEqual([5, 2, undefined]); - })); + expect(facade.getSubscriptionList).toHaveBeenCalledWith( + expect.anything(), + 2, + undefined + ); + }); }); diff --git a/feature-libs/subscription-billing/components/product/price/subscription-product-price.component.spec.ts b/feature-libs/subscription-billing/components/product/price/subscription-product-price.component.spec.ts index fb16b5ea541..656cdaf35e4 100644 --- a/feature-libs/subscription-billing/components/product/price/subscription-product-price.component.spec.ts +++ b/feature-libs/subscription-billing/components/product/price/subscription-product-price.component.spec.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Product, TranslatePipe, TranslationService } from '@spartacus/core'; import { CurrentProductService } from '@spartacus/storefront'; import { @@ -8,6 +8,7 @@ import { SubscriptionProductService, } from '@spartacus/subscription-billing/root'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { SubscriptionProductPriceComponent } from './subscription-product-price.component'; const mockOneTime: OneTimeCharge[] = [{ name: 'one' }, { name: 'two' }]; const mockRecurring: RecurringCharge[] = [{ price: { value: 1 } }]; @@ -55,7 +56,7 @@ describe('SubscriptionProductPriceComponent', () => { let component: SubscriptionProductPriceComponent; let fixture: ComponentFixture; let productService: SubscriptionProductService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [SubscriptionProductPriceComponent], providers: [ @@ -73,11 +74,11 @@ describe('SubscriptionProductPriceComponent', () => { }) .compileComponents(); productService = TestBed.inject(SubscriptionProductService); - })); + }); describe('for a null product', () => { beforeEach(() => { - spyOn(productService, 'getSubscriptionData').and.returnValue(of(null)); - spyOn(productService, 'isSubscription').and.returnValue(true); + vi.spyOn(productService, 'getSubscriptionData').mockReturnValue(of(null)); + vi.spyOn(productService, 'isSubscription').mockReturnValue(true); fixture = TestBed.createComponent(SubscriptionProductPriceComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -99,8 +100,8 @@ describe('SubscriptionProductPriceComponent', () => { }); describe('for a mock product without price plan', () => { beforeEach(() => { - spyOn(productService, 'isSubscription').and.returnValue(true); - spyOn(productService, 'getSubscriptionData').and.returnValue( + vi.spyOn(productService, 'isSubscription').mockReturnValue(true); + vi.spyOn(productService, 'getSubscriptionData').mockReturnValue( of(mockProduct1) ); fixture = TestBed.createComponent(SubscriptionProductPriceComponent); @@ -125,8 +126,8 @@ describe('SubscriptionProductPriceComponent', () => { }); describe('for a mock product with price plan', () => { beforeEach(() => { - spyOn(productService, 'isSubscription').and.returnValue(true); - spyOn(productService, 'getSubscriptionData').and.returnValue( + vi.spyOn(productService, 'isSubscription').mockReturnValue(true); + vi.spyOn(productService, 'getSubscriptionData').mockReturnValue( of(mockProduct2) ); fixture = TestBed.createComponent(SubscriptionProductPriceComponent); diff --git a/feature-libs/subscription-billing/components/product/usage/subscription-product-usage-charge.component.spec.ts b/feature-libs/subscription-billing/components/product/usage/subscription-product-usage-charge.component.spec.ts index 090ac6e5e29..e0245fe96af 100644 --- a/feature-libs/subscription-billing/components/product/usage/subscription-product-usage-charge.component.spec.ts +++ b/feature-libs/subscription-billing/components/product/usage/subscription-product-usage-charge.component.spec.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform, signal } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslatePipe, TranslationService } from '@spartacus/core'; import { Observable, of } from 'rxjs'; import { UsageChargeType } from '../../../root/model'; @@ -77,7 +77,7 @@ const mockProduct2 = signal({ sapPricePlan: {} }); describe('SubscriptionProductUsageChargeComponent', () => { let component: SubscriptionProductUsageChargeComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [SubscriptionProductUsageChargeComponent], providers: [ @@ -91,7 +91,7 @@ describe('SubscriptionProductUsageChargeComponent', () => { .compileComponents(); fixture = TestBed.createComponent(SubscriptionProductUsageChargeComponent); component = fixture.componentInstance; - })); + }); it('should be created', () => { Object.defineProperty(component, 'product', { get: () => signal(null), diff --git a/feature-libs/subscription-billing/core/connector/subscription-actions.connector.spec.ts b/feature-libs/subscription-billing/core/connector/subscription-actions.connector.spec.ts index e8573df0039..4193524c6c9 100644 --- a/feature-libs/subscription-billing/core/connector/subscription-actions.connector.spec.ts +++ b/feature-libs/subscription-billing/core/connector/subscription-actions.connector.spec.ts @@ -1,25 +1,26 @@ import { TestBed } from '@angular/core/testing'; import { SubscriptionActionsConnector } from './subscription-actions.connector'; import { SubscriptionActionsAdapter } from './subscription-actions.adapter'; -import { of, throwError } from 'rxjs'; +import { of, firstValueFrom, throwError } from 'rxjs'; import { SubscriptionCancellationDetails, SubscriptionWithdraw, } from '@spartacus/subscription-billing/root'; +import { vi } from 'vitest'; describe('SubscriptionActionsConnector', () => { let connector: SubscriptionActionsConnector; - let adapter: jasmine.SpyObj; + let adapter: any; beforeEach(() => { - const adapterSpy = jasmine.createSpyObj('SubscriptionActionsAdapter', [ - 'getEffectiveCancellationDate', - 'cancelSubscription', - 'reverseCancellation', - 'withdrawSubscription', - 'extendSubscription', - 'getExtensionEffectiveDate', - ]); + const adapterSpy = { + getEffectiveCancellationDate: vi.fn(), + cancelSubscription: vi.fn(), + reverseCancellation: vi.fn(), + withdrawSubscription: vi.fn(), + extendSubscription: vi.fn(), + getExtensionEffectiveDate: vi.fn(), + }; TestBed.configureTestingModule({ providers: [ @@ -29,9 +30,7 @@ describe('SubscriptionActionsConnector', () => { }); connector = TestBed.inject(SubscriptionActionsConnector); - adapter = TestBed.inject( - SubscriptionActionsAdapter - ) as jasmine.SpyObj; + adapter = TestBed.inject(SubscriptionActionsAdapter) as any; }); it('should be created', () => { @@ -44,7 +43,7 @@ describe('SubscriptionActionsConnector', () => { const subscriptionCode = 'subABC'; const expectedResponse = of({ date: '2025-01-01' }); - adapter.getEffectiveCancellationDate.and.returnValue(expectedResponse); + adapter.getEffectiveCancellationDate.mockReturnValue(expectedResponse); const result = connector.getEffectiveCancellationDate( userId, @@ -64,7 +63,7 @@ describe('SubscriptionActionsConnector', () => { const subscriptionCode = 'subABC'; const expectedResponse = of({ subscriptionEndAt: '2025-01-01' }); - adapter.getExtensionEffectiveDate.and.returnValue(expectedResponse); + adapter.getExtensionEffectiveDate.mockReturnValue(expectedResponse); const result = connector.getExtensionEffectiveDate( userId, @@ -91,7 +90,7 @@ describe('SubscriptionActionsConnector', () => { }; const expectedResponse = of({ success: true }); - adapter.cancelSubscription.and.returnValue(expectedResponse); + adapter.cancelSubscription.mockReturnValue(expectedResponse); const result = connector.cancelSubscription( userId, @@ -106,7 +105,7 @@ describe('SubscriptionActionsConnector', () => { expect(result).toBe(expectedResponse); }); - it('should handle errors', (done) => { + it('should handle errors', async () => { const userId = 'user123'; const subscriptionCode = 'subABC'; const cancellationDetails: SubscriptionCancellationDetails = { @@ -114,16 +113,17 @@ describe('SubscriptionActionsConnector', () => { }; const error = new Error('Cancel error'); - adapter.cancelSubscription.and.returnValue(throwError(() => error)); - - connector - .cancelSubscription(userId, subscriptionCode, cancellationDetails) - .subscribe({ - error: (e) => { - expect(e).toBe(error); - done(); - }, - }); + adapter.cancelSubscription.mockReturnValue(throwError(() => error)); + + await expect( + firstValueFrom( + connector.cancelSubscription( + userId, + subscriptionCode, + cancellationDetails + ) + ) + ).rejects.toBe(error); }); }); @@ -133,7 +133,7 @@ describe('SubscriptionActionsConnector', () => { const subscriptionCode = 'subABC'; const expectedResponse = of({ success: true }); - adapter.extendSubscription.and.returnValue(expectedResponse); + adapter.extendSubscription.mockReturnValue(expectedResponse); const result = connector.extendSubscription( userId, @@ -150,21 +150,18 @@ describe('SubscriptionActionsConnector', () => { expect(result).toBe(expectedResponse); }); - it('should handle errors', (done) => { + it('should handle errors', async () => { const userId = 'user123'; const subscriptionCode = 'subABC'; const error = new Error('Cancel error'); - adapter.extendSubscription.and.returnValue(throwError(() => error)); + adapter.extendSubscription.mockReturnValue(throwError(() => error)); - connector - .extendSubscription(userId, subscriptionCode, 1, false) - .subscribe({ - error: (e) => { - expect(e).toBe(error); - done(); - }, - }); + await expect( + firstValueFrom( + connector.extendSubscription(userId, subscriptionCode, 1, false) + ) + ).rejects.toBe(error); }); }); @@ -174,7 +171,7 @@ describe('SubscriptionActionsConnector', () => { const subscriptionCode = 'subABC'; const expectedResponse = of({ reversed: true }); - adapter.reverseCancellation.and.returnValue(expectedResponse); + adapter.reverseCancellation.mockReturnValue(expectedResponse); const result = connector.reverseCancellation(userId, subscriptionCode); expect(adapter.reverseCancellation).toHaveBeenCalledWith( @@ -197,7 +194,7 @@ describe('SubscriptionActionsConnector', () => { }; const expectedResponse = of({ withdrawn: true }); - adapter.withdrawSubscription.and.returnValue(expectedResponse); + adapter.withdrawSubscription.mockReturnValue(expectedResponse); const result = connector.withdrawSubscription( userId, diff --git a/feature-libs/subscription-billing/core/connector/subscription-billing.connector.spec.ts b/feature-libs/subscription-billing/core/connector/subscription-billing.connector.spec.ts index fc6cc5a5669..77d0b3474f2 100644 --- a/feature-libs/subscription-billing/core/connector/subscription-billing.connector.spec.ts +++ b/feature-libs/subscription-billing/core/connector/subscription-billing.connector.spec.ts @@ -1,11 +1,12 @@ import { TestBed } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { SubscriptionBillingAdapter } from './subscription-billing.adapter'; import { SubscriptionBillingConnector } from './subscription-billing.connector'; import { SubscriptionBill, SubscriptionBillsList, } from '@spartacus/subscription-billing/root'; +import { vi } from 'vitest'; const mockBillData: SubscriptionBill = { billAt: '2026-04-11T00:00:00+0000', @@ -92,13 +93,13 @@ const listWithData: SubscriptionBillsList = { describe('SubscriptionBillingConnector', () => { let connector: SubscriptionBillingConnector; - let adapter: jasmine.SpyObj; + let adapter: any; beforeEach(() => { - const adapterSpy = jasmine.createSpyObj('SubscriptionBillingAdapter', [ - 'getSubscriptionBillsList', - 'getSubscriptionBillByCode', - ]); + const adapterSpy = { + getSubscriptionBillsList: vi.fn(), + getSubscriptionBillByCode: vi.fn(), + }; TestBed.configureTestingModule({ providers: [ @@ -108,9 +109,7 @@ describe('SubscriptionBillingConnector', () => { }); connector = TestBed.inject(SubscriptionBillingConnector); - adapter = TestBed.inject( - SubscriptionBillingAdapter - ) as jasmine.SpyObj; + adapter = TestBed.inject(SubscriptionBillingAdapter) as any; }); it('should be created', () => { @@ -122,7 +121,7 @@ describe('SubscriptionBillingConnector', () => { const userId = 'current'; const expectedResponse = of(listWithData); - adapter.getSubscriptionBillsList.and.returnValue(expectedResponse); + adapter.getSubscriptionBillsList.mockReturnValue(expectedResponse); const result = connector.getSubscriptionBillsList(userId, 5, 1); expect(adapter.getSubscriptionBillsList).toHaveBeenCalledWith( @@ -135,18 +134,15 @@ describe('SubscriptionBillingConnector', () => { expect(result).toBe(expectedResponse); }); - it('should handle errors', (done) => { + it('should handle errors', async () => { const userId = 'current'; const error = new Error('Cancel error'); - adapter.getSubscriptionBillsList.and.returnValue(throwError(() => error)); + adapter.getSubscriptionBillsList.mockReturnValue(throwError(() => error)); - connector.getSubscriptionBillsList(userId, 5, 1).subscribe({ - error: (e) => { - expect(e).toBe(error); - done(); - }, - }); + await expect( + firstValueFrom(connector.getSubscriptionBillsList(userId, 5, 1)) + ).rejects.toBe(error); }); }); @@ -155,7 +151,7 @@ describe('SubscriptionBillingConnector', () => { const userId = 'current'; const expectedResponse = of(mockBillData); - adapter.getSubscriptionBillByCode.and.returnValue(expectedResponse); + adapter.getSubscriptionBillByCode.mockReturnValue(expectedResponse); const result = connector.getSubscriptionBillByCode( userId, @@ -168,22 +164,22 @@ describe('SubscriptionBillingConnector', () => { expect(result).toBe(expectedResponse); }); - it('should handle errors', (done) => { + it('should handle errors', async () => { const userId = 'current'; const error = new Error('Cancel error'); - adapter.getSubscriptionBillByCode.and.returnValue( + adapter.getSubscriptionBillByCode.mockReturnValue( throwError(() => error) ); - connector - .getSubscriptionBillByCode(userId, mockBillData.documentNumber ?? '') - .subscribe({ - error: (e) => { - expect(e).toBe(error); - done(); - }, - }); + await expect( + firstValueFrom( + connector.getSubscriptionBillByCode( + userId, + mockBillData.documentNumber ?? '' + ) + ) + ).rejects.toBe(error); }); }); }); diff --git a/feature-libs/subscription-billing/core/connector/subscription.connector.spec.ts b/feature-libs/subscription-billing/core/connector/subscription.connector.spec.ts index 5d92d026826..9b88d594b85 100644 --- a/feature-libs/subscription-billing/core/connector/subscription.connector.spec.ts +++ b/feature-libs/subscription-billing/core/connector/subscription.connector.spec.ts @@ -1,12 +1,12 @@ import { TestBed } from '@angular/core/testing'; import { SubscriptionAdapter } from './subscription.adapter'; import { SubscriptionConnector } from './subscription.connector'; -import createSpy = jasmine.createSpy; import { SubscriptionDetail, SubscriptionList, } from '@spartacus/subscription-billing/root'; import { of, take } from 'rxjs'; +import { vi } from 'vitest'; const mockDetail: SubscriptionDetail = { id: '01', documentNumber: '2081', @@ -57,8 +57,8 @@ const mockList: SubscriptionList = { ], }; class MockSubscriptionAdapter implements Partial { - getSubscriptionByCode = createSpy().and.returnValue(of(mockDetail)); - getSubscriptionList = createSpy().and.returnValue(of(mockList)); + getSubscriptionByCode = vi.fn().mockReturnValue(of(mockDetail)); + getSubscriptionList = vi.fn().mockReturnValue(of(mockList)); } describe('SubscriptionConnector', () => { let service: SubscriptionConnector; diff --git a/feature-libs/subscription-billing/core/facade/subscription-actions.service.spec.ts b/feature-libs/subscription-billing/core/facade/subscription-actions.service.spec.ts index 21feb80626d..8a4854fa0d3 100644 --- a/feature-libs/subscription-billing/core/facade/subscription-actions.service.spec.ts +++ b/feature-libs/subscription-billing/core/facade/subscription-actions.service.spec.ts @@ -10,37 +10,36 @@ import { GetSubscriptionByCodeReloadEvent, SubscriptionWithdraw, } from '@spartacus/subscription-billing/root'; -import { of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { Store } from '@ngrx/store'; +import { vi } from 'vitest'; const mockRoutingService = { - go: jasmine.createSpy('go'), + go: vi.fn(), }; const mockStore = { - dispatch: jasmine.createSpy(), - pipe: jasmine.createSpy().and.returnValue(of({})), + dispatch: vi.fn(), + pipe: vi.fn().mockReturnValue(of({})), }; describe('SubscriptionActionsService', () => { let service: SubscriptionActionsService; - let userIdService: jasmine.SpyObj; - let cancelConnector: jasmine.SpyObj; - let subscriptionConnector: jasmine.SpyObj; + let userIdService: any; + let cancelConnector: any; + let subscriptionConnector: any; const userId = 'user123'; const subscriptionCode = 'sub456'; beforeEach(() => { - userIdService = jasmine.createSpyObj('UserIdService', ['getUserId']); - cancelConnector = jasmine.createSpyObj('SubscriptionActionsConnector', [ - 'getEffectiveCancellationDate', - 'cancelSubscription', - 'reverseCancellation', - 'withdrawSubscription', - ]); - subscriptionConnector = jasmine.createSpyObj('SubscriptionConnector', [ - 'check', - ]); + userIdService = { getUserId: vi.fn() }; + cancelConnector = { + getEffectiveCancellationDate: vi.fn(), + cancelSubscription: vi.fn(), + reverseCancellation: vi.fn(), + withdrawSubscription: vi.fn(), + }; + subscriptionConnector = { check: vi.fn() }; TestBed.configureTestingModule({ providers: [ SubscriptionActionsService, @@ -66,54 +65,44 @@ describe('SubscriptionActionsService', () => { }); describe('getEffectiveCancellationDate', () => { - it('should call connector with correct params', (done) => { - userIdService.getUserId.and.returnValue(of(userId)); - cancelConnector.getEffectiveCancellationDate.and.returnValue( + it('should call connector with correct params', async () => { + userIdService.getUserId.mockReturnValue(of(userId)); + cancelConnector.getEffectiveCancellationDate.mockReturnValue( of('mockDate') ); - service - .getEffectiveCancellationDate(subscriptionCode) - .subscribe((res) => { - expect( - cancelConnector.getEffectiveCancellationDate - ).toHaveBeenCalledWith(userId, subscriptionCode); - expect(res).toBe('mockDate'); - done(); - }); + const res = await firstValueFrom( + service.getEffectiveCancellationDate(subscriptionCode) + ); + expect(cancelConnector.getEffectiveCancellationDate).toHaveBeenCalledWith( + userId, + subscriptionCode + ); + expect(res).toBe('mockDate'); }); - it('should emit error when userId or code is missing', (done) => { - userIdService.getUserId.and.returnValue(of(null as any)); - - service - .cancelSubscription({ subscriptionEndAt: '2026-01-01' }, undefined) - .subscribe({ - next: () => { - fail('Expected an error, but got a value'); - done(); - }, - error: (err) => { - expect(err.message).toBe( - 'Cannot cancel subscription: missing user ID or subscription code.' - ); - done(); - }, - }); + it('should emit error when userId or code is missing', async () => { + userIdService.getUserId.mockReturnValue(of(null as any)); + + await expect( + firstValueFrom( + service.cancelSubscription( + { subscriptionEndAt: '2026-01-01' }, + undefined + ) + ) + ).rejects.toMatchObject({ + message: + 'Cannot cancel subscription: missing user ID or subscription code.', + }); }); - it('should emit error when userId or subscriptionCode is missing in getEffectiveCancellationDate', (done) => { - userIdService.getUserId.and.returnValue(of(null as any)); - - service.getEffectiveCancellationDate(undefined).subscribe({ - next: () => { - fail('Expected an error, but got a value'); - done(); - }, - error: (err) => { - expect(err.message).toBe( - 'Cannot fetch cancellation effective date: missing user ID or subscription code.' - ); - done(); - }, + it('should emit error when userId or subscriptionCode is missing in getEffectiveCancellationDate', async () => { + userIdService.getUserId.mockReturnValue(of(null as any)); + + await expect( + firstValueFrom(service.getEffectiveCancellationDate(undefined)) + ).rejects.toMatchObject({ + message: + 'Cannot fetch cancellation effective date: missing user ID or subscription code.', }); }); }); @@ -123,70 +112,58 @@ describe('SubscriptionActionsService', () => { subscriptionEndAt: '2026-01-01', }; - it('should call connector with correct params', (done) => { - userIdService.getUserId.and.returnValue(of(userId)); - cancelConnector.cancelSubscription.and.returnValue(of('success')); - - service - .cancelSubscription(cancellationDetails, subscriptionCode) - .subscribe((res) => { - expect(cancelConnector.cancelSubscription).toHaveBeenCalledWith( - userId, - subscriptionCode, - cancellationDetails - ); - expect(res).toBe('success'); - done(); - }); + it('should call connector with correct params', async () => { + userIdService.getUserId.mockReturnValue(of(userId)); + cancelConnector.cancelSubscription.mockReturnValue(of('success')); + + const res = await firstValueFrom( + service.cancelSubscription(cancellationDetails, subscriptionCode) + ); + expect(cancelConnector.cancelSubscription).toHaveBeenCalledWith( + userId, + subscriptionCode, + cancellationDetails + ); + expect(res).toBe('success'); }); - it('should emit error when userId or code is missing', (done) => { - userIdService.getUserId.and.returnValue(of(null as any)); + it('should emit error when userId or code is missing', async () => { + userIdService.getUserId.mockReturnValue(of(null as any)); - service.cancelSubscription(cancellationDetails, undefined).subscribe({ - next: () => { - fail('Expected an error, but got a value'); - done(); - }, - error: (err) => { - expect(err.message).toBe( - 'Cannot cancel subscription: missing user ID or subscription code.' - ); - done(); - }, + await expect( + firstValueFrom( + service.cancelSubscription(cancellationDetails, undefined) + ) + ).rejects.toMatchObject({ + message: + 'Cannot cancel subscription: missing user ID or subscription code.', }); }); }); describe('reverseCancellation', () => { - it('should call connector with correct params', (done) => { - userIdService.getUserId.and.returnValue(of(userId)); - cancelConnector.reverseCancellation.and.returnValue(of('reversed')); - - service.reverseCancellation(subscriptionCode).subscribe((res) => { - expect(cancelConnector.reverseCancellation).toHaveBeenCalledWith( - userId, - subscriptionCode - ); - expect(res).toBe('reversed'); - done(); - }); + it('should call connector with correct params', async () => { + userIdService.getUserId.mockReturnValue(of(userId)); + cancelConnector.reverseCancellation.mockReturnValue(of('reversed')); + + const res = await firstValueFrom( + service.reverseCancellation(subscriptionCode) + ); + expect(cancelConnector.reverseCancellation).toHaveBeenCalledWith( + userId, + subscriptionCode + ); + expect(res).toBe('reversed'); }); - it('should emit error when userId or code is missing', (done) => { - userIdService.getUserId.and.returnValue(of(null as any)); + it('should emit error when userId or code is missing', async () => { + userIdService.getUserId.mockReturnValue(of(null as any)); - service.reverseCancellation(undefined).subscribe({ - next: () => { - fail('Expected an error, but got a value'); - done(); - }, - error: (err) => { - expect(err.message).toBe( - 'Cannot reverse cancellation: missing user ID or subscription code.' - ); - done(); - }, + await expect( + firstValueFrom(service.reverseCancellation(undefined)) + ).rejects.toMatchObject({ + message: + 'Cannot reverse cancellation: missing user ID or subscription code.', }); }); }); @@ -199,37 +176,29 @@ describe('SubscriptionActionsService', () => { withdrawalPeriodEndDate: '2025-07-15', }; - it('should call connector with correct params', (done) => { - userIdService.getUserId.and.returnValue(of(userId)); - cancelConnector.withdrawSubscription.and.returnValue(of('withdrawn')); - - service - .withdrawSubscription(withdrawalData, subscriptionCode) - .subscribe((res) => { - expect(cancelConnector.withdrawSubscription).toHaveBeenCalledWith( - userId, - subscriptionCode, - withdrawalData - ); - expect(res).toBe('withdrawn'); - done(); - }); + it('should call connector with correct params', async () => { + userIdService.getUserId.mockReturnValue(of(userId)); + cancelConnector.withdrawSubscription.mockReturnValue(of('withdrawn')); + + const res = await firstValueFrom( + service.withdrawSubscription(withdrawalData, subscriptionCode) + ); + expect(cancelConnector.withdrawSubscription).toHaveBeenCalledWith( + userId, + subscriptionCode, + withdrawalData + ); + expect(res).toBe('withdrawn'); }); - it('should emit error when userId or code is missing', (done) => { - userIdService.getUserId.and.returnValue(of(null as any)); + it('should emit error when userId or code is missing', async () => { + userIdService.getUserId.mockReturnValue(of(null as any)); - service.withdrawSubscription(withdrawalData, undefined).subscribe({ - next: () => { - fail('Expected an error, but got a value'); - done(); - }, - error: (err) => { - expect(err.message).toBe( - 'Cannot withdraw subscription: missing user ID or subscription code.' - ); - done(); - }, + await expect( + firstValueFrom(service.withdrawSubscription(withdrawalData, undefined)) + ).rejects.toMatchObject({ + message: + 'Cannot withdraw subscription: missing user ID or subscription code.', }); }); }); diff --git a/feature-libs/subscription-billing/core/facade/subscription-billing.service.spec.ts b/feature-libs/subscription-billing/core/facade/subscription-billing.service.spec.ts index f29da5dae12..339edaa0ace 100644 --- a/feature-libs/subscription-billing/core/facade/subscription-billing.service.spec.ts +++ b/feature-libs/subscription-billing/core/facade/subscription-billing.service.spec.ts @@ -6,12 +6,12 @@ import { OCC_USER_ID_CURRENT, } from '@spartacus/core'; import { SubscriptionBillingConnector } from '../connector'; -import createSpy = jasmine.createSpy; -import { of, take, tap } from 'rxjs'; +import { firstValueFrom, of, take, tap } from 'rxjs'; import { SubscriptionBill, SubscriptionBillsList, } from '@spartacus/subscription-billing/root'; +import { vi } from 'vitest'; const mockUserId = OCC_USER_ID_CURRENT; const mockRouteState = { state: { @@ -89,13 +89,13 @@ class MockUserIdService implements Partial { } class MockRoutingService implements Partial { - getRouterState = createSpy().and.returnValue(of(mockRouteState)); + getRouterState = vi.fn().mockReturnValue(of(mockRouteState)); } class MockSubscriptionConnector implements Partial { - getSubscriptionBillByCode = createSpy().and.returnValue(of(mockDetail)); - getSubscriptionBillsList = createSpy().and.returnValue(of(mockList)); + getSubscriptionBillByCode = vi.fn().mockReturnValue(of(mockDetail)); + getSubscriptionBillsList = vi.fn().mockReturnValue(of(mockList)); } describe('SubscriptionBillingService', () => { let service: SubscriptionBillingService; @@ -128,95 +128,89 @@ describe('SubscriptionBillingService', () => { const mockPageSize = 5; const mockSort = 'byBillingDateDesc'; - it('should call connectors getSubscriptionBillsList', (done) => { - service - .getSubscriptionBillsList(mockPageSize, mockCurrentPage, mockSort) - .pipe(take(1)) - .subscribe((data) => { - expect(connector.getSubscriptionBillsList).toHaveBeenCalledWith( - mockUserId, - mockPageSize, - mockCurrentPage, - mockSort, - undefined - ); - expect(data).toEqual(mockList); - done(); - }); + it('should call connectors getSubscriptionBillsList', async () => { + const data = await firstValueFrom( + service.getSubscriptionBillsList( + mockPageSize, + mockCurrentPage, + mockSort + ) + ); + expect(connector.getSubscriptionBillsList).toHaveBeenCalledWith( + mockUserId, + mockPageSize, + mockCurrentPage, + mockSort, + undefined + ); + expect(data).toEqual(mockList); }); - it('should contain the query state', (done) => { + it('should contain the query state', async () => { const mockCurrentPage = 1; const mockPageSize = 5; const mockSort = 'byBillingDateDesc'; - service - .getSubscriptionBillsListState( + const state = await firstValueFrom( + service.getSubscriptionBillsListState( mockCurrentPage, mockPageSize, mockSort, undefined ) - .pipe(take(1)) - .subscribe((state) => { - expect(connector.getSubscriptionBillsList).toHaveBeenCalledWith( - mockUserId, - mockCurrentPage, - mockPageSize, - mockSort, - undefined - ); - expect(state).toEqual({ - loading: false, - error: false, - data: mockList, - }); - done(); - }); + ); + expect(connector.getSubscriptionBillsList).toHaveBeenCalledWith( + mockUserId, + mockCurrentPage, + mockPageSize, + mockSort, + undefined + ); + expect(state).toEqual({ + loading: false, + error: false, + data: mockList, + }); }); }); describe('getSubscriptionBillList with empty userId', () => { const mockCurrentPage = 1; const mockPageSize = 5; const mockSort = 'byBillingDateDesc'; - it('should return error observable because of no userId', (done) => { - spyOn(userIdServiceSpy, 'getUserId').and.returnValue(of('')); + it('should return error observable because of no userId', async () => { + vi.spyOn(userIdServiceSpy, 'getUserId').mockReturnValue(of('')); service = TestBed.inject(SubscriptionBillingService); connector = TestBed.inject(SubscriptionBillingConnector); - service - .getSubscriptionBillsListState( - mockPageSize, - mockCurrentPage, - mockSort, - undefined - ) - .pipe( - tap(() => { - expect(connector.getSubscriptionBillsList).not.toHaveBeenCalled(); - }) - ) - .subscribe((_) => { - done(); - }); + await firstValueFrom( + service + .getSubscriptionBillsListState( + mockPageSize, + mockCurrentPage, + mockSort, + undefined + ) + .pipe( + tap(() => { + expect(connector.getSubscriptionBillsList).not.toHaveBeenCalled(); + }) + ) + ); }); - it('should contain the query state', (done) => { - service - .getSubscriptionBillByCodeState() - .pipe(take(1)) - .subscribe((state) => { - expect(connector.getSubscriptionBillByCode).toHaveBeenCalledWith( - mockUserId, - '01' - ); - expect(state).toEqual({ - loading: false, - error: false, - data: mockDetail, - }); - done(); - }); + it('should contain the query state', async () => { + const state = await firstValueFrom( + service.getSubscriptionBillByCodeState() + ); + expect(connector.getSubscriptionBillByCode).toHaveBeenCalledWith( + mockUserId, + '01' + ); + expect(state).toEqual({ + loading: false, + error: false, + data: mockDetail, + }); }); }); }); diff --git a/feature-libs/subscription-billing/core/facade/subscription.service.spec.ts b/feature-libs/subscription-billing/core/facade/subscription.service.spec.ts index 2b5fadef910..6d56c835fcd 100644 --- a/feature-libs/subscription-billing/core/facade/subscription.service.spec.ts +++ b/feature-libs/subscription-billing/core/facade/subscription.service.spec.ts @@ -6,12 +6,12 @@ import { OCC_USER_ID_CURRENT, } from '@spartacus/core'; import { SubscriptionConnector } from '../connector'; -import createSpy = jasmine.createSpy; -import { of, take } from 'rxjs'; +import { firstValueFrom, of, take } from 'rxjs'; import { SubscriptionDetail, SubscriptionList, } from '@spartacus/subscription-billing/root'; +import { vi } from 'vitest'; const mockUserId = OCC_USER_ID_CURRENT; const mockRouteState = { state: { @@ -76,15 +76,15 @@ const mockList: SubscriptionList = { ], }; class MockUserIdService implements Partial { - getUserId = createSpy().and.returnValue(of(mockUserId)); + getUserId = vi.fn().mockReturnValue(of(mockUserId)); } class MockRoutingService implements Partial { - getRouterState = createSpy().and.returnValue(of(mockRouteState)); + getRouterState = vi.fn().mockReturnValue(of(mockRouteState)); } class MockSubscriptionConnector implements Partial { - getSubscriptionByCode = createSpy().and.returnValue(of(mockDetail)); - getSubscriptionList = createSpy().and.returnValue(of(mockList)); + getSubscriptionByCode = vi.fn().mockReturnValue(of(mockDetail)); + getSubscriptionList = vi.fn().mockReturnValue(of(mockList)); } describe('SubscriptionService', () => { let service: SubscriptionService; @@ -115,77 +115,65 @@ describe('SubscriptionService', () => { const mockPageSize = 5; const mockSort = 'byId'; - it('should call connector.getSubscriptionList', (done) => { - service - .getSubscriptionList(mockCurrentPage, mockPageSize, mockSort) - .pipe(take(1)) - .subscribe((data) => { - expect(connector.getSubscriptionList).toHaveBeenCalledWith( - mockUserId, - mockCurrentPage, - mockPageSize, - mockSort - ); - expect(data).toEqual(mockList); - done(); - }); + it('should call connector.getSubscriptionList', async () => { + const data = await firstValueFrom( + service.getSubscriptionList(mockCurrentPage, mockPageSize, mockSort) + ); + expect(connector.getSubscriptionList).toHaveBeenCalledWith( + mockUserId, + mockCurrentPage, + mockPageSize, + mockSort + ); + expect(data).toEqual(mockList); }); - it('should contain the query state', (done) => { + it('should contain the query state', async () => { const mockCurrentPage = 1; const mockPageSize = 5; const mockSort = 'byId'; - service - .getSubscriptionListState(mockCurrentPage, mockPageSize, mockSort) - .pipe(take(1)) - .subscribe((state) => { - expect(connector.getSubscriptionList).toHaveBeenCalledWith( - mockUserId, - mockCurrentPage, - mockPageSize, - mockSort - ); - expect(state).toEqual({ - loading: false, - error: false, - data: mockList, - }); - done(); - }); + const state = await firstValueFrom( + service.getSubscriptionListState( + mockCurrentPage, + mockPageSize, + mockSort + ) + ); + expect(connector.getSubscriptionList).toHaveBeenCalledWith( + mockUserId, + mockCurrentPage, + mockPageSize, + mockSort + ); + expect(state).toEqual({ + loading: false, + error: false, + data: mockList, + }); }); }); describe('getSubscriptionByCode', () => { - it('should call connector.getSubscriptionByCode', (done) => { - service - .getSubscriptionByCode() - .pipe(take(1)) - .subscribe((data) => { - expect(connector.getSubscriptionByCode).toHaveBeenCalledWith( - mockUserId, - '01' - ); - expect(data).toEqual(mockDetail); - done(); - }); + it('should call connector.getSubscriptionByCode', async () => { + const data = await firstValueFrom(service.getSubscriptionByCode()); + expect(connector.getSubscriptionByCode).toHaveBeenCalledWith( + mockUserId, + '01' + ); + expect(data).toEqual(mockDetail); }); - it('should contain the query state', (done) => { - service - .getSubscriptionByCodeState() - .pipe(take(1)) - .subscribe((state) => { - expect(connector.getSubscriptionByCode).toHaveBeenCalledWith( - mockUserId, - '01' - ); - expect(state).toEqual({ - loading: false, - error: false, - data: mockDetail, - }); - done(); - }); + it('should contain the query state', async () => { + const state = await firstValueFrom(service.getSubscriptionByCodeState()); + expect(connector.getSubscriptionByCode).toHaveBeenCalledWith( + mockUserId, + '01' + ); + expect(state).toEqual({ + loading: false, + error: false, + data: mockDetail, + }); }); }); }); diff --git a/feature-libs/subscription-billing/karma.conf.js b/feature-libs/subscription-billing/karma.conf.js deleted file mode 100644 index 0ececda0024..00000000000 --- a/feature-libs/subscription-billing/karma.conf.js +++ /dev/null @@ -1,49 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots'], - coverageReporter: { - dir: require('path').join( - __dirname, - '../../coverage/subscription-billing' - ), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 85, - functions: 85, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/subscription-billing/occ/adapters/occ-subscription-actions.adapter.spec.ts b/feature-libs/subscription-billing/occ/adapters/occ-subscription-actions.adapter.spec.ts index 88245a37db1..1ac129557c5 100644 --- a/feature-libs/subscription-billing/occ/adapters/occ-subscription-actions.adapter.spec.ts +++ b/feature-libs/subscription-billing/occ/adapters/occ-subscription-actions.adapter.spec.ts @@ -9,11 +9,12 @@ import { SubscriptionCancellationDetails, SubscriptionWithdraw as Withdrawal, } from '@spartacus/subscription-billing/root'; +import { vi } from 'vitest'; describe('OccSubscriptionActionsAdapter', () => { let adapter: OccSubscriptionActionsAdapter; let httpMock: HttpTestingController; - let occEndpointsService: jasmine.SpyObj; + let occEndpointsService: any; const mockUserId = 'testUser'; const mockSubscriptionCode = 'testSubscription'; @@ -30,9 +31,7 @@ describe('OccSubscriptionActionsAdapter', () => { }; beforeEach(() => { - const occEndpointsSpy = jasmine.createSpyObj('OccEndpointsService', [ - 'buildUrl', - ]); + const occEndpointsSpy = { buildUrl: vi.fn() }; TestBed.configureTestingModule({ imports: [HttpClientTestingModule], @@ -44,9 +43,7 @@ describe('OccSubscriptionActionsAdapter', () => { adapter = TestBed.inject(OccSubscriptionActionsAdapter); httpMock = TestBed.inject(HttpTestingController); - occEndpointsService = TestBed.inject( - OccEndpointsService - ) as jasmine.SpyObj; + occEndpointsService = TestBed.inject(OccEndpointsService) as any; }); afterEach(() => { @@ -55,7 +52,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should cancel subscription', () => { const mockUrl = 'mockUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); adapter .cancelSubscription( @@ -73,7 +70,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should fetch cancellation subscription effective date', () => { const mockUrl = 'mockEffectiveDateUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); adapter .getEffectiveCancellationDate(mockUserId, mockSubscriptionCode) @@ -86,7 +83,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should fetch extension effective date', () => { const mockUrl = 'mockEffectiveDateUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); adapter .getExtensionEffectiveDate(mockUserId, mockSubscriptionCode, 1, false) @@ -99,7 +96,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should handle withdrawal', () => { const mockUrl = 'mockWithdrawalUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); adapter .withdrawSubscription(mockUserId, mockSubscriptionCode, mockWithdrawal) @@ -113,7 +110,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should reverse cancellation', () => { const mockUrl = 'mockReverseCancellationUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); adapter.reverseCancellation(mockUserId, mockSubscriptionCode).subscribe(); @@ -125,7 +122,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should extend subscription', () => { const mockUrl = 'mockExtendSubscriptionUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); adapter .extendSubscription(mockUserId, mockSubscriptionCode, 1, false) @@ -142,7 +139,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should handle error when cancelSubscription fails', () => { const mockUrl = 'mockUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); const mockHttpError = { status: 500, @@ -172,7 +169,7 @@ describe('OccSubscriptionActionsAdapter', () => { }); it('should handle error when getEffectiveCancellationDate fails', () => { const mockUrl = 'mockEffectiveDateUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); const mockHttpError = { status: 404, @@ -199,7 +196,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should handle error when getExtensionEffectiveDate fails', () => { const mockUrl = 'mockEffectiveDateUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); const mockHttpError = { status: 404, @@ -226,7 +223,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should handle error when withdrawal fails', () => { const mockUrl = 'mockWithdrawalUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); const mockHttpError = { status: 400, @@ -252,7 +249,7 @@ describe('OccSubscriptionActionsAdapter', () => { }); it('should handle error when reverseCancellation fails', () => { const mockUrl = 'mockReverseCancellationUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); const mockHttpError = { status: 500, @@ -277,7 +274,7 @@ describe('OccSubscriptionActionsAdapter', () => { it('should handle error when extend subsription fails', () => { const mockUrl = 'mockExtendSubscriptionUrl'; - occEndpointsService.buildUrl.and.returnValue(mockUrl); + occEndpointsService.buildUrl.mockReturnValue(mockUrl); const mockHttpError = { status: 500, diff --git a/feature-libs/subscription-billing/occ/adapters/occ-subscription-billing-adapter.spec.ts b/feature-libs/subscription-billing/occ/adapters/occ-subscription-billing-adapter.spec.ts index b2f2836183e..864d7fd03e5 100644 --- a/feature-libs/subscription-billing/occ/adapters/occ-subscription-billing-adapter.spec.ts +++ b/feature-libs/subscription-billing/occ/adapters/occ-subscription-billing-adapter.spec.ts @@ -12,7 +12,7 @@ import { SubscriptionBill, SubscriptionBillsList, } from '@spartacus/subscription-billing/root'; -import { take } from 'rxjs'; +import { firstValueFrom, take } from 'rxjs'; import { defaultOccSubscriptionBillingConfig } from '../config/default-occ-subscription-billing-config'; import { OccSubscriptionBillingAdapter } from './occ-subscription-billing.adapter'; @@ -124,17 +124,13 @@ describe('OccSubscriptionBillingAdapter', () => { }); describe('getSubscriptionBillByCode', () => { - it('should get subscription bill for the given bill id', (done) => { - service - .getSubscriptionBillByCode( + it('should get subscription bill for the given bill id', async () => { + const resultPromise = firstValueFrom( + service.getSubscriptionBillByCode( mockCustomerId, mockBillData.documentNumber ?? '' ) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockBillData); - done(); - }); + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -146,22 +142,26 @@ describe('OccSubscriptionBillingAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockBillData); + + const result = await resultPromise; + expect(result).toEqual(mockBillData); }); }); describe('getSubscriptionBillsList', () => { - it('should get list of subscription bills for the given customer id', (done) => { + it('should get list of subscription bills for the given customer id', async () => { const PAGE_SIZE = 5; const currentPage = 1; const sort = 'byBillingDateDesc'; - service - .getSubscriptionBillsList(mockCustomerId, PAGE_SIZE, currentPage, sort) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockListData); - done(); - }); + const resultPromise = firstValueFrom( + service.getSubscriptionBillsList( + mockCustomerId, + PAGE_SIZE, + currentPage, + sort + ) + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -174,6 +174,9 @@ describe('OccSubscriptionBillingAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockListData); + + const result = await resultPromise; + expect(result).toEqual(mockListData); }); }); }); diff --git a/feature-libs/subscription-billing/occ/adapters/occ-subscription.adapter.spec.ts b/feature-libs/subscription-billing/occ/adapters/occ-subscription.adapter.spec.ts index e0a57da2ae0..df1461447ef 100644 --- a/feature-libs/subscription-billing/occ/adapters/occ-subscription.adapter.spec.ts +++ b/feature-libs/subscription-billing/occ/adapters/occ-subscription.adapter.spec.ts @@ -12,7 +12,7 @@ import { SubscriptionDetail, SubscriptionList, } from '@spartacus/subscription-billing/root'; -import { take } from 'rxjs'; +import { firstValueFrom, take } from 'rxjs'; import { OccSubscriptionAdapter } from './occ-subscription.adapter'; const mockDetail: SubscriptionDetail = { id: '01', @@ -92,14 +92,10 @@ describe('OccSubscriptionAdapter', () => { }); describe('getSubscriptionByCode', () => { - it('should get subscription details for the given subscription id', (done) => { - service - .getSubscriptionByCode(mockCustomerId, mockSubscriptionId) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockDetail); - done(); - }); + it('should get subscription details for the given subscription id', async () => { + const resultPromise = firstValueFrom( + service.getSubscriptionByCode(mockCustomerId, mockSubscriptionId) + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -111,22 +107,26 @@ describe('OccSubscriptionAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockDetail); + + const result = await resultPromise; + expect(result).toEqual(mockDetail); }); }); describe('getSubscriptionList', () => { - it('should get list of subscriptions for the given customer id', (done) => { + it('should get list of subscriptions for the given customer id', async () => { const PAGE_SIZE = 5; const currentPage = 1; const sort = 'byId'; - service - .getSubscriptionList(mockCustomerId, PAGE_SIZE, currentPage, sort) - .pipe(take(1)) - .subscribe((result) => { - expect(result).toEqual(mockList); - done(); - }); + const resultPromise = firstValueFrom( + service.getSubscriptionList( + mockCustomerId, + PAGE_SIZE, + currentPage, + sort + ) + ); const mockReq = httpMock.expectOne((req) => { return ( @@ -139,6 +139,9 @@ describe('OccSubscriptionAdapter', () => { expect(mockReq.cancelled).toBeFalsy(); expect(mockReq.request.responseType).toEqual('json'); mockReq.flush(mockList); + + const result = await resultPromise; + expect(result).toEqual(mockList); }); }); }); diff --git a/feature-libs/subscription-billing/project.json b/feature-libs/subscription-billing/project.json index d0dd4695e4c..44e0530f4be 100644 --- a/feature-libs/subscription-billing/project.json +++ b/feature-libs/subscription-billing/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/subscription-billing/test.ts", - "tsConfig": "feature-libs/subscription-billing/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/subscription-billing/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/subscription-billing/root/events/subscription-billing-event.listener.spec.ts b/feature-libs/subscription-billing/root/events/subscription-billing-event.listener.spec.ts index dbaf4dffec2..9702938e9cf 100644 --- a/feature-libs/subscription-billing/root/events/subscription-billing-event.listener.spec.ts +++ b/feature-libs/subscription-billing/root/events/subscription-billing-event.listener.spec.ts @@ -1,5 +1,4 @@ import { Subject } from 'rxjs'; -import createSpy = jasmine.createSpy; import { CurrencySetEvent, CxEvent, @@ -12,12 +11,13 @@ import { GetSubscriptionByCodeReloadEvent, GetSubscriptionListReloadEvent, } from './subscription-billing.events'; +import { vi } from 'vitest'; const mockEventStream$ = new Subject(); class MockEventService implements Partial { - get = createSpy().and.returnValue(mockEventStream$.asObservable()); - dispatch = createSpy(); + get = vi.fn().mockReturnValue(mockEventStream$.asObservable()); + dispatch = vi.fn(); } describe('SubscriptionBillingEventListener', () => { diff --git a/feature-libs/subscription-billing/test.ts b/feature-libs/subscription-billing/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/subscription-billing/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/subscription-billing/tsconfig.spec.json b/feature-libs/subscription-billing/tsconfig.spec.json index 24c03719595..52ed5f1b870 100644 --- a/feature-libs/subscription-billing/tsconfig.spec.json +++ b/feature-libs/subscription-billing/tsconfig.spec.json @@ -2,11 +2,18 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "target": "es2022", "module": "preserve", - "types": ["jasmine", "node"], - "moduleResolution": "bundler" + "target": "es2022", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/subscription-billing/vitest.config.ts b/feature-libs/subscription-billing/vitest.config.ts new file mode 100644 index 00000000000..2eb352a3095 --- /dev/null +++ b/feature-libs/subscription-billing/vitest.config.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/subscription-billing`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-subscription-billing.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/tracking/karma.conf.js b/feature-libs/tracking/karma.conf.js deleted file mode 100644 index 8da314fe0b7..00000000000 --- a/feature-libs/tracking/karma.conf.js +++ /dev/null @@ -1,49 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-tracking.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/tracking'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 70, - functions: 90, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/tracking/personalization/core/services/personalization-context.service.spec.ts b/feature-libs/tracking/personalization/core/services/personalization-context.service.spec.ts index 1dd5a108de7..81cbbfbcfcf 100644 --- a/feature-libs/tracking/personalization/core/services/personalization-context.service.spec.ts +++ b/feature-libs/tracking/personalization/core/services/personalization-context.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { CmsService, Page, PageType } from '@spartacus/core'; import { PersonalizationConfig } from '@spartacus/tracking/personalization/root'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { PersonalizationContext } from '../model/personalization-context.model'; import { PersonalizationContextService } from './personalization-context.service'; @@ -104,7 +105,7 @@ describe('PersonalizationContextService', () => { }); it('should return undefined if PersonalizationScriptComponent does not exists', () => { - spyOn(cmsService, 'getCurrentPage').and.returnValue( + vi.spyOn(cmsService, 'getCurrentPage').mockReturnValue( of({ slots: { PlaceholderContentSlot: {}, diff --git a/feature-libs/tracking/project.json b/feature-libs/tracking/project.json index f3bdd16f082..6a8c9777007 100644 --- a/feature-libs/tracking/project.json +++ b/feature-libs/tracking/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/tracking/test.ts", - "tsConfig": "feature-libs/tracking/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/tracking/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/tracking/test.ts b/feature-libs/tracking/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/tracking/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/tracking/tms/aep/services/aep-collector.service.spec.ts b/feature-libs/tracking/tms/aep/services/aep-collector.service.spec.ts index 10f236228d1..a97374c4eaf 100644 --- a/feature-libs/tracking/tms/aep/services/aep-collector.service.spec.ts +++ b/feature-libs/tracking/tms/aep/services/aep-collector.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { LoginEvent, ScriptLoader } from '@spartacus/core'; import { WindowObject } from '@spartacus/tracking/tms/core'; +import { vi } from 'vitest'; import '../config/default-aep.config'; import { AepCollectorConfig } from '../config/default-aep.config'; import { AepCollectorService } from './aep-collector.service'; @@ -39,7 +40,7 @@ describe('AepCollectorService', () => { }); it('should embed the script tag', () => { - spyOn(scriptLoader, 'embedScript').and.stub(); + vi.spyOn(scriptLoader, 'embedScript').mockImplementation(() => {}); const windowObject = {} as WindowObject; service.init(config, windowObject); expect(scriptLoader.embedScript).toHaveBeenCalledTimes(1); diff --git a/feature-libs/tracking/tms/core/services/tms.service.spec.ts b/feature-libs/tracking/tms/core/services/tms.service.spec.ts index 7f019c44316..2bca44307b5 100644 --- a/feature-libs/tracking/tms/core/services/tms.service.spec.ts +++ b/feature-libs/tracking/tms/core/services/tms.service.spec.ts @@ -7,6 +7,7 @@ import { WindowRef, } from '@spartacus/core'; import { Observable, of } from 'rxjs'; +import { vi } from 'vitest'; import { TmsCollectorConfig, TmsConfig } from '../config/tms-config'; import { TmsCollector, WindowObject } from '../model/tms.model'; import { TmsService } from './tms.service'; @@ -69,12 +70,12 @@ describe('TmsService', () => { gtmCollector = TestBed.inject(GtmCollectorMock); aepCollector = TestBed.inject(AepCollectorMock); - spyOn(gtmCollector, 'init').and.callThrough(); - spyOn(gtmCollector, 'map').and.callThrough(); - spyOn(gtmCollector, 'pushEvent').and.callThrough(); - spyOn(aepCollector, 'init').and.callThrough(); - spyOn(aepCollector, 'map').and.callThrough(); - spyOn(aepCollector, 'pushEvent').and.callThrough(); + vi.spyOn(gtmCollector, 'init'); + vi.spyOn(gtmCollector, 'map'); + vi.spyOn(gtmCollector, 'pushEvent'); + vi.spyOn(aepCollector, 'init'); + vi.spyOn(aepCollector, 'map'); + vi.spyOn(aepCollector, 'pushEvent'); }); it('should be created', () => { diff --git a/feature-libs/tracking/tms/gtm/services/gtm-collector.service.spec.ts b/feature-libs/tracking/tms/gtm/services/gtm-collector.service.spec.ts index e0684ca5738..69d3c72fd5d 100644 --- a/feature-libs/tracking/tms/gtm/services/gtm-collector.service.spec.ts +++ b/feature-libs/tracking/tms/gtm/services/gtm-collector.service.spec.ts @@ -1,6 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { LoginEvent, WindowRef } from '@spartacus/core'; import { WindowObject } from '@spartacus/tracking/tms/core'; +import { vi } from 'vitest'; import '../config/default-gtm.config'; import { GtmCollectorConfig } from '../config/default-gtm.config'; import { GtmCollectorService } from './gtm-collector.service'; @@ -52,8 +53,8 @@ describe('GtmCollectorService', () => { }); it('should embed the script tag', () => { - spyOn(winRef.document, 'getElementsByTagName').and.callThrough(); - spyOn(winRef.document, 'createElement').and.callThrough(); + vi.spyOn(winRef.document, 'getElementsByTagName'); + vi.spyOn(winRef.document, 'createElement'); const windowObject = {} as WindowObject; service.init(config, windowObject); diff --git a/feature-libs/tracking/tsconfig.spec.json b/feature-libs/tracking/tsconfig.spec.json index 26fbc5e271a..d52c68cbde6 100644 --- a/feature-libs/tracking/tsconfig.spec.json +++ b/feature-libs/tracking/tsconfig.spec.json @@ -4,9 +4,16 @@ "outDir": "../../out-tsc/spec", "module": "preserve", "strict": false, - "types": ["jasmine"], - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true }, - "files": ["./test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": ["vitest.config.ts", "**/*.spec.ts", "**/*.d.ts"] } diff --git a/feature-libs/tracking/vitest.config.ts b/feature-libs/tracking/vitest.config.ts new file mode 100644 index 00000000000..84271c3dee7 --- /dev/null +++ b/feature-libs/tracking/vitest.config.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/tracking`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-tracking.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/user/account/components/guards/login-as-guest.guard.spec.ts b/feature-libs/user/account/components/guards/login-as-guest.guard.spec.ts index e8a30a40337..aa84fa852f2 100644 --- a/feature-libs/user/account/components/guards/login-as-guest.guard.spec.ts +++ b/feature-libs/user/account/components/guards/login-as-guest.guard.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; //generate test for LoginAsGuestGuard import { TestBed } from '@angular/core/testing'; @@ -17,13 +18,13 @@ const mockFeatureToggles: FeatureToggles = { const mockWindowRef = { localStorage: { - getItem: jasmine.createSpy().and.returnValue('true'), - removeItem: jasmine.createSpy(), + getItem: vi.fn().mockReturnValue('true'), + removeItem: vi.fn(), }, }; const mockSemanticPathService = { - get: jasmine.createSpy().and.returnValue('loginForm'), + get: vi.fn().mockReturnValue('loginForm'), }; describe('LoginAsGuestGuard', () => { @@ -52,7 +53,7 @@ describe('LoginAsGuestGuard', () => { }); beforeEach(() => { - mockWindowRef.localStorage.removeItem.calls.reset(); + mockWindowRef.localStorage.removeItem.mockClear(); }); it('should be created', () => { @@ -84,9 +85,7 @@ describe('LoginAsGuestGuard', () => { it('should return true if IS_GUEST_USER_CHECKOUT_KEY is not set to true', () => { featureToggles.authorizationCodeFlowByDefault = true; - (mockWindowRef.localStorage?.getItem as jasmine.Spy).and.returnValue( - 'false' - ); + (mockWindowRef.localStorage?.getItem as any).mockReturnValue('false'); guard.canActivate().subscribe((result) => { expect(result).toBe(true); }); @@ -98,9 +97,7 @@ describe('LoginAsGuestGuard', () => { it('should return true if IS_GUEST_USER_CHECKOUT_KEY is not set', () => { featureToggles.authorizationCodeFlowByDefault = true; - (mockWindowRef.localStorage?.getItem as jasmine.Spy).and.returnValue( - null - ); + (mockWindowRef.localStorage?.getItem as any).mockReturnValue(null); guard.canActivate().subscribe((result) => { expect(result).toBe(true); }); diff --git a/feature-libs/user/account/components/login-form/login-form-component.service.spec.ts b/feature-libs/user/account/components/login-form/login-form-component.service.spec.ts index d0107162717..56d7358094c 100644 --- a/feature-libs/user/account/components/login-form/login-form-component.service.spec.ts +++ b/feature-libs/user/account/components/login-form/login-form-component.service.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { Provider } from '@angular/core'; -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, @@ -30,19 +31,11 @@ import { SESSION_EXPIRED_ERROR, } from '../user-account-constants'; import { LoginFormComponentService } from './login-form-component.service'; -import createSpy = jasmine.createSpy; class MockWinRef { - localStorage = jasmine.createSpyObj('localStorage', [ - 'setItem', - 'removeItem', - ]); + localStorage = { setItem: vi.fn(), removeItem: vi.fn() }; - sessionStorage = jasmine.createSpyObj('sessionStorage', [ - 'setItem', - 'getItem', - 'removeItem', - ]); + sessionStorage = { setItem: vi.fn(), getItem: vi.fn(), removeItem: vi.fn() }; location = { href: '' } as Location; @@ -56,17 +49,17 @@ class MockWinRef { } class MockAuthService implements Partial { - loginWithCredentials = createSpy().and.returnValue(of({})); - isUserLoggedIn = createSpy().and.returnValue(of(true)); - loginWithRedirect = createSpy().and.returnValue(true); - getCsrfToken = createSpy().and.returnValue( + loginWithCredentials = vi.fn().mockReturnValue(of({})); + isUserLoggedIn = vi.fn().mockReturnValue(of(true)); + loginWithRedirect = vi.fn().mockReturnValue(true); + getCsrfToken = vi.fn().mockReturnValue( of({ headerName: 'CSFR', parameterName: '_csfr', token: 'token', }) ); - refreshCsrfToken = createSpy().and.returnValue( + refreshCsrfToken = vi.fn().mockReturnValue( of({ headerName: 'CSFR', parameterName: '_csfr', @@ -76,8 +69,8 @@ class MockAuthService implements Partial { } class MockGlobalMessageService { - add = createSpy().and.stub(); - remove = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); + remove = vi.fn().mockImplementation(() => {}); } class MockFederatedLoginService implements Partial { @@ -97,8 +90,8 @@ class MockActivatedRoute implements Partial { } class MockRouter implements Partial { - navigate = createSpy().and.stub(); - navigateByUrl = createSpy().and.stub(); + navigate = vi.fn().mockImplementation(() => {}); + navigateByUrl = vi.fn().mockImplementation(() => {}); } class MockAuthConfigService implements Partial { @@ -108,8 +101,8 @@ class MockAuthConfigService implements Partial { } class MockCsrfStateService implements Partial { - get = createSpy().and.returnValue({ token: 'token' }); - set = createSpy().and.stub(); + get = vi.fn().mockReturnValue({ token: 'token' }); + set = vi.fn().mockImplementation(() => {}); } class MockAuthMultisiteIsolationService @@ -171,13 +164,13 @@ describe('LoginFormComponentService', () => { provideMockFeatureToggles({ ...mockFeatureToggles }), ]; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, I18nTestingModule, FormErrorsModule], declarations: [], providers: [...providers], }).compileComponents(); - })); + }); beforeEach(() => { service = TestBed.inject(LoginFormComponentService); @@ -191,16 +184,20 @@ describe('LoginFormComponentService', () => { globalMessageService = TestBed.inject(GlobalMessageService); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should create service', () => { expect(service).toBeTruthy(); }); describe('showResetPassword', () => { it('should be true when isLoginDomain is false', () => { - expect(service.showResetPassword).toBeTrue(); + expect(service.showResetPassword).toBe(true); }); - it('should be false when isLoginDomain is true', waitForAsync(() => { + it('should be false when isLoginDomain is true', async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [ReactiveFormsModule, I18nTestingModule, FormErrorsModule], @@ -212,7 +209,7 @@ describe('LoginFormComponentService', () => { service = TestBed.inject(LoginFormComponentService); expect(service.showResetPassword).toBe(false); - })); + }); }); describe('login', () => { @@ -225,7 +222,7 @@ describe('LoginFormComponentService', () => { }); it('should patch user id', () => { - spyOnProperty(winRef, 'nativeWindow', 'get').and.returnValue({ + vi.spyOn(winRef, 'nativeWindow', 'get').mockReturnValue({ history: { state: { newUid: 'test.user@shop.com' } }, } as Window); service.isUpdating$.subscribe().unsubscribe(); @@ -249,7 +246,7 @@ describe('LoginFormComponentService', () => { }); it('should reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.login(); expect(service.form.reset).toHaveBeenCalled(); }); @@ -269,7 +266,7 @@ describe('LoginFormComponentService', () => { }); it('should not reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.login(); expect(service.form.reset).not.toHaveBeenCalled(); }); @@ -278,12 +275,16 @@ describe('LoginFormComponentService', () => { describe('new flow', () => { let mockFeatureTogglesController: MockFeatureTogglesController; // Reset test module to reconfigure FeatureToggles - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [...providers], - }).compileComponents(); - })); + }); + TestBed.overrideProvider(FeatureToggles, { + useFactory: () => TestBed.inject(MockFeatureTogglesController), + }); + await TestBed.compileComponents(); + }); beforeEach(() => { mockFeatureTogglesController = TestBed.inject( @@ -321,16 +322,16 @@ describe('LoginFormComponentService', () => { }); }); - it('should submit native form with refreshed CSRF token', waitForAsync(() => { + it('should submit native form with refreshed CSRF token', async () => { const form = createForm(userId, password, csrf); - const submitSpy = spyOn(form, 'submit'); + const submitSpy = vi.spyOn(form, 'submit'); service.login(form); expect(submitSpy).toHaveBeenCalledWith(); expect(winRef.localStorage?.setItem).toHaveBeenCalledWith( OAUTH_REDIRECT_FLOW_KEY, 'true' ); - })); + }); describe('when siteIsolationForCustomLoginPage is enabled', () => { beforeEach(() => { @@ -347,14 +348,14 @@ describe('LoginFormComponentService', () => { csrf: 'token', }; const decoratedUserId = testData.userId + '|decorator'; - spyOn( + vi.spyOn( authMultisiteIsolationService, 'decorateUserId' - ).and.returnValue(of(decoratedUserId)); + ).mockReturnValue(of(decoratedUserId)); service.form.setValue(testData); const form = createForm(userId, password, csrf); let submittedFormData: FormData; - spyOn(form, 'submit').and.callFake(() => { + vi.spyOn(form, 'submit').mockImplementation(() => { submittedFormData = new FormData(form); }); @@ -366,27 +367,27 @@ describe('LoginFormComponentService', () => { }); }); - it('should update csrf form field with fresh token before submit', waitForAsync(() => { + it('should update csrf form field with fresh token before submit', async () => { service.form.get('csrf')?.setValue('old-token'); const form = createForm(userId, password, 'old-token'); - spyOn(form, 'submit'); + vi.spyOn(form, 'submit'); service.login(form); expect(service.form.get('csrf')?.value).toBe('new-token'); - })); + }); - it('should not disable the form before submitting (browser drops disabled inputs from POST body)', waitForAsync(() => { + it('should not disable the form before submitting (browser drops disabled inputs from POST body)', async () => { const form = createForm(userId, password, csrf); let formDisabledAtSubmit: boolean | undefined; - spyOn(form, 'submit').and.callFake(() => { + vi.spyOn(form, 'submit').mockImplementation(() => { formDisabledAtSubmit = service.form.disabled; }); service.login(form); expect(form.submit).toHaveBeenCalled(); expect(formDisabledAtSubmit).toBe(false); - })); + }); it('should reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.login(); expect(service.form.reset).toHaveBeenCalled(); }); @@ -407,13 +408,13 @@ describe('LoginFormComponentService', () => { it('should not login', () => { const form = createForm(userId, password, csrf); - const submitSpy = spyOn(form, 'submit'); + const submitSpy = vi.spyOn(form, 'submit'); service.login(form); expect(submitSpy).not.toHaveBeenCalled(); }); it('should not reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); const form = createForm(userId, password, csrf); service.login(form); expect(service.form.reset).not.toHaveBeenCalled(); @@ -426,22 +427,22 @@ describe('LoginFormComponentService', () => { const csrf = 'token'; beforeEach(() => { - (authService.refreshCsrfToken as jasmine.Spy).and.returnValue( + (authService.refreshCsrfToken as any).mockReturnValue( throwError(() => ({ status: 403 })) ); service.form.setValue({ userId, password, csrf }); }); - it('should NOT submit the form', waitForAsync(() => { + it('should NOT submit the form', async () => { const form = createForm(userId, password, csrf); - const submitSpy = spyOn(form, 'submit'); + const submitSpy = vi.spyOn(form, 'submit'); service.login(form); expect(submitSpy).not.toHaveBeenCalled(); - })); + }); - it('should stash session_expired in sessionStorage and hard-redirect to /login on CSRF refresh failure', waitForAsync(() => { + it('should stash session_expired in sessionStorage and hard-redirect to /login on CSRF refresh failure', async () => { const form = createForm(userId, password, csrf); - spyOn(form, 'submit'); + vi.spyOn(form, 'submit'); service.login(form); expect(winRef.sessionStorage?.setItem).toHaveBeenCalledWith( LOGIN_ERROR_KEY, @@ -449,32 +450,32 @@ describe('LoginFormComponentService', () => { ); expect(winRef.nativeWindow?.location.href).toBe('/login'); expect(authService.loginWithRedirect).not.toHaveBeenCalled(); - })); + }); - it('should reset busy state to false on CSRF refresh failure', waitForAsync(() => { + it('should reset busy state to false on CSRF refresh failure', async () => { const form = createForm(userId, password, csrf); - spyOn(form, 'submit'); + vi.spyOn(form, 'submit'); let busyValue: boolean | undefined; service.isUpdating$.subscribe((v) => (busyValue = v)); service.login(form); expect(busyValue).toBe(false); - })); + }); - it('should clear the OAuth redirect flow flag on CSRF refresh failure', waitForAsync(() => { + it('should clear the OAuth redirect flow flag on CSRF refresh failure', async () => { const form = createForm(userId, password, csrf); - spyOn(form, 'submit'); + vi.spyOn(form, 'submit'); service.login(form); expect(winRef.localStorage?.removeItem).toHaveBeenCalledWith( OAUTH_REDIRECT_FLOW_KEY ); - })); + }); - it('should surface the session-expired message inline when nativeWindow is unexpectedly undefined (defensive fallback)', waitForAsync(() => { - spyOnProperty(winRef, 'nativeWindow', 'get').and.returnValue( + it('should surface the session-expired message inline when nativeWindow is unexpectedly undefined (defensive fallback)', async () => { + vi.spyOn(winRef, 'nativeWindow', 'get').mockReturnValue( undefined as unknown as Window ); const form = createForm(userId, password, csrf); - spyOn(form, 'submit'); + vi.spyOn(form, 'submit'); service.login(form); expect(winRef.sessionStorage?.setItem).toHaveBeenCalledWith( LOGIN_ERROR_KEY, @@ -484,18 +485,22 @@ describe('LoginFormComponentService', () => { { key: 'httpHandlers.sessionExpired' }, GlobalMessageType.MSG_TYPE_ERROR ); - })); + }); }); describe('when authorizationCodeFlowByDefaultCsrfTokenRefresh is disabled', () => { let mockFeatureTogglesController: MockFeatureTogglesController; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [...providers], - }).compileComponents(); - })); + }); + TestBed.overrideProvider(FeatureToggles, { + useFactory: () => TestBed.inject(MockFeatureTogglesController), + }); + await TestBed.compileComponents(); + }); beforeEach(() => { mockFeatureTogglesController = TestBed.inject( @@ -525,7 +530,7 @@ describe('LoginFormComponentService', () => { csrf: 'token', }); const form = createForm('test@email.com', 'secret', 'token'); - const submitSpy = spyOn(form, 'submit'); + const submitSpy = vi.spyOn(form, 'submit'); service.login(form); expect(submitSpy).toHaveBeenCalled(); expect(authService.refreshCsrfToken).not.toHaveBeenCalled(); @@ -546,10 +551,10 @@ describe('LoginFormComponentService', () => { csrf: 'token', }; const decoratedUserId = testData.userId + '|decorator'; - spyOn( + vi.spyOn( authMultisiteIsolationService, 'decorateUserId' - ).and.returnValue(of(decoratedUserId)); + ).mockReturnValue(of(decoratedUserId)); service.form.setValue(testData); const form = createForm( testData.userId, @@ -557,7 +562,7 @@ describe('LoginFormComponentService', () => { testData.csrf ); let submittedFormData: FormData; - spyOn(form, 'submit').and.callFake(() => { + vi.spyOn(form, 'submit').mockImplementation(() => { submittedFormData = new FormData(form); }); @@ -592,7 +597,7 @@ describe('LoginFormComponentService', () => { }); it('should drain a session_expired stash from sessionStorage and surface httpHandlers.sessionExpired', () => { - (winRef.sessionStorage?.getItem as jasmine.Spy).and.callFake( + (winRef.sessionStorage?.getItem as any).mockImplementation( (key: string) => key === LOGIN_ERROR_KEY ? SESSION_EXPIRED_ERROR : null ); @@ -635,13 +640,13 @@ describe('LoginFormComponentService', () => { const csrf = 'token'; beforeEach(() => { - spyOn(winRef, 'isBrowser').and.returnValue(false); + vi.spyOn(winRef, 'isBrowser').mockReturnValue(false); service.form.setValue({ userId, password, csrf }); }); it('should not set localStorage flag when submitting login form', () => { const form = createForm(userId, password, csrf); - spyOn(form, 'submit'); + vi.spyOn(form, 'submit'); service.login(form); expect(winRef.localStorage?.setItem).not.toHaveBeenCalled(); }); diff --git a/feature-libs/user/account/components/login-form/login-form.component.spec.ts b/feature-libs/user/account/components/login-form/login-form.component.spec.ts index c2bdd135533..6bdb6d554e3 100644 --- a/feature-libs/user/account/components/login-form/login-form.component.spec.ts +++ b/feature-libs/user/account/components/login-form/login-form.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { DebugElement, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -21,7 +22,6 @@ import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feat import { BehaviorSubject } from 'rxjs'; import { LoginFormComponentService } from './login-form-component.service'; import { LoginFormComponent } from './login-form.component'; -import createSpy = jasmine.createSpy; const isBusySubject = new BehaviorSubject(false); class MockLoginFormComponentService @@ -32,8 +32,8 @@ class MockLoginFormComponentService password: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - login = createSpy().and.stub(); - handleCustomLoginError = createSpy().and.stub(); + login = vi.fn().mockImplementation(() => {}); + handleCustomLoginError = vi.fn().mockImplementation(() => {}); showResetPassword = true; } @Pipe({ name: 'cxUrl' }) @@ -47,7 +47,7 @@ describe('LoginFormComponent', () => { let el: DebugElement; let service: LoginFormComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -78,7 +78,7 @@ describe('LoginFormComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(LoginFormComponent); @@ -147,7 +147,7 @@ describe('LoginFormComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); diff --git a/feature-libs/user/account/components/login-register/login-register.component.spec.ts b/feature-libs/user/account/components/login-register/login-register.component.spec.ts index 0e24a9d513d..8f4c64d99dd 100644 --- a/feature-libs/user/account/components/login-register/login-register.component.spec.ts +++ b/feature-libs/user/account/components/login-register/login-register.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { @@ -41,7 +42,7 @@ describe('LoginRegisterComponent', () => { fixture.detectChanges(); } - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useClass: MockActivatedRoute }, @@ -57,7 +58,7 @@ describe('LoginRegisterComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { createComponent(); @@ -125,7 +126,7 @@ describe('LoginRegisterComponent', () => { }); it('should navigate to register', () => { - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); const registerLink = getRegisterLink(); registerLink.triggerEventHandler('click'); @@ -162,7 +163,7 @@ describe('LoginRegisterComponent', () => { TestBed.compileComponents(); createComponent(); callNgInit(); - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); const guestLinkElement = getGuestCheckoutLink(); guestLinkElement.triggerEventHandler('click'); diff --git a/feature-libs/user/account/components/login/login.component.spec.ts b/feature-libs/user/account/components/login/login.component.spec.ts index 849b00eee32..cb29fc9814b 100644 --- a/feature-libs/user/account/components/login/login.component.spec.ts +++ b/feature-libs/user/account/components/login/login.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; import { @@ -17,7 +18,6 @@ import { PageSlotComponent } from '@spartacus/storefront'; import { UserAccountFacade } from '@spartacus/user/account/root'; import { Observable, of } from 'rxjs'; import { LoginComponent } from './login.component'; -import createSpy = jasmine.createSpy; const mockUserDetails: User = { displayUid: 'Display Uid', @@ -28,7 +28,7 @@ const mockUserDetails: User = { }; class MockAuthService { - login = createSpy(); + login = vi.fn(); isUserLoggedIn(): Observable { return of(true); } @@ -37,7 +37,7 @@ class MockAuthService { } } class MockRoutingService { - go = createSpy('go'); + go = vi.fn(); } class MockUserAccountFacade { get(): Observable { @@ -78,7 +78,7 @@ describe('LoginComponent', () => { let authService: AuthService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [LoginComponent, I18nTestingModule], providers: [ @@ -115,13 +115,12 @@ describe('LoginComponent', () => { .compileComponents(); authService = TestBed.inject(AuthService); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; component.ngOnInit(); - fixture.detectChanges(); }); it('should be created', () => { @@ -141,7 +140,7 @@ describe('LoginComponent', () => { }); it('should not get user details when token is lacking', () => { - spyOn(authService, 'isUserLoggedIn').and.returnValue(of(false)); + vi.spyOn(authService, 'isUserLoggedIn').mockReturnValue(of(false)); let user; component.ngOnInit(); @@ -162,25 +161,29 @@ describe('LoginComponent', () => { }); it('should display greeting message when the user is logged in', () => { - expect(fixture.debugElement.nativeElement.innerText).toContain( + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.textContent?.trim()).toContain( expectedGreeting ); }); it('should display the register message when the user is not logged in', () => { - spyOn(authService, 'isUserLoggedIn').and.returnValue(of(false)); + vi.spyOn(authService, 'isUserLoggedIn').mockReturnValue(of(false)); component.ngOnInit(); fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.innerText).toContain( + expect(fixture.debugElement.nativeElement.textContent?.trim()).toContain( 'miniLogin.signInRegister' ); }); it('should contain the dynamic slot: HeaderLinks', () => { - spyOn(component, 'onRootNavBtnAdded').and.callThrough(); + const spy = vi + .spyOn(component, 'onRootNavBtnAdded') + .mockImplementation(() => {}); component.ngOnInit(); fixture.detectChanges(); + spy.mockRestore(); expectedGreeting = 'Testing;'; const expectedRootNavBtn = fixture.debugElement.query( By.css('cx-navigation-ui nav ul li:first-child button') @@ -195,25 +198,25 @@ describe('LoginComponent', () => { }); it('should display login when using asm client', () => { - spyOn(authService, 'isUsingASMClient').and.returnValue(of(false)); - spyOn(authService, 'isUserLoggedIn').and.returnValue(of(false)); + vi.spyOn(authService, 'isUsingASMClient').mockReturnValue(of(false)); + vi.spyOn(authService, 'isUserLoggedIn').mockReturnValue(of(false)); component.ngOnInit(); fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.innerText).toContain( + expect(fixture.debugElement.nativeElement.textContent?.trim()).toContain( 'miniLogin.signInRegister' ); }); it('should not display login when using asm client', () => { - spyOn(authService, 'isUsingASMClient').and.returnValue(of(true)); - spyOn(authService, 'isUserLoggedIn').and.returnValue(of(false)); + vi.spyOn(authService, 'isUsingASMClient').mockReturnValue(of(true)); + vi.spyOn(authService, 'isUserLoggedIn').mockReturnValue(of(false)); component.ngOnInit(); fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.innerText).not.toContain( - 'miniLogin.signInRegister' - ); + expect( + fixture.debugElement.nativeElement.textContent?.trim() + ).not.toContain('miniLogin.signInRegister'); }); }); }); diff --git a/feature-libs/user/account/components/my-account-v2-user/my-account-v2-user.component.spec.ts b/feature-libs/user/account/components/my-account-v2-user/my-account-v2-user.component.spec.ts index f6aaa16ea45..18712d57cb3 100644 --- a/feature-libs/user/account/components/my-account-v2-user/my-account-v2-user.component.spec.ts +++ b/feature-libs/user/account/components/my-account-v2-user/my-account-v2-user.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; @@ -15,10 +16,9 @@ import { MockUrlPipe } from 'core-libs/core/src/routing/configurable-routes/url- import { Observable, of } from 'rxjs'; import { UserAccountFacade } from '../../root/facade'; import { MyAccountV2UserComponent } from './my-account-v2-user.component'; -import createSpy = jasmine.createSpy; class MockAuthService { - login = createSpy(); + login = vi.fn(); isUserLoggedIn(): Observable { return of(true); } @@ -36,7 +36,7 @@ const mockUserDetails: User = { }; class MockRoutingService { - go = createSpy('go'); + go = vi.fn(); } class MockUserAccountFacade { get(): Observable { diff --git a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts index a119d989382..6b4f35a3967 100644 --- a/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts +++ b/feature-libs/user/account/components/otp-login-form/otp-login-form.component.spec.ts @@ -1,6 +1,7 @@ +import { vi } from 'vitest'; import { HttpErrorResponse } from '@angular/common/http'; import { DebugElement, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; @@ -20,7 +21,6 @@ import { import { of, throwError } from 'rxjs'; import { OTP_LOGIN_STATE_STORAGE_KEY } from '../user-account-constants'; import { OneTimePasswordLoginFormComponent } from './otp-login-form.component'; -import createSpy = jasmine.createSpy; const verificationTokenCreation: VerificationTokenCreation = { purpose: 'LOGIN', @@ -38,7 +38,7 @@ class MockWinRef { } class MockRoutingService { - go = createSpy(); + go = vi.fn(); } @Pipe({ name: 'cxUrl' }) @@ -54,7 +54,7 @@ describe('OneTimePasswordLoginFormComponent', () => { let winRef: WindowRef; let mockRoutingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -78,7 +78,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { winRef = TestBed.inject(WindowRef); @@ -96,18 +96,18 @@ describe('OneTimePasswordLoginFormComponent', () => { describe('ngOnInit', () => { it('should restore form values from sessionStorage', () => { - const storageSpy = jasmine.createSpyObj('Storage', [ - 'getItem', - 'setItem', - 'removeItem', - ]); - storageSpy.getItem.and.callFake((key) => + const storageSpy = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + storageSpy.getItem.mockImplementation((key) => key === OTP_LOGIN_STATE_STORAGE_KEY ? JSON.stringify({ loginId: 'test@email.com' }) : null ); - spyOnProperty(winRef, 'sessionStorage', 'get').and.returnValue( - storageSpy + vi.spyOn(winRef, 'sessionStorage', 'get').mockReturnValue( + storageSpy as any ); component.ngOnInit(); expect(component.form.value.userId).toEqual('test@email.com'); @@ -115,14 +115,14 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should not patch form when sessionStorage has no credentials', () => { - const storageSpy = jasmine.createSpyObj('Storage', [ - 'getItem', - 'setItem', - 'removeItem', - ]); - storageSpy.getItem.and.returnValue(null); - spyOnProperty(winRef, 'sessionStorage', 'get').and.returnValue( - storageSpy + const storageSpy = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + storageSpy.getItem.mockReturnValue(null); + vi.spyOn(winRef, 'sessionStorage', 'get').mockReturnValue( + storageSpy as any ); component.ngOnInit(); expect(component.form.value.userId).toEqual(''); @@ -130,18 +130,18 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should clear sessionStorage after reading', () => { - const storageSpy = jasmine.createSpyObj('Storage', [ - 'getItem', - 'setItem', - 'removeItem', - ]); - storageSpy.getItem.and.callFake((key) => + const storageSpy = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + storageSpy.getItem.mockImplementation((key) => key === OTP_LOGIN_STATE_STORAGE_KEY ? JSON.stringify({ loginId: 'test@email.com' }) : null ); - spyOnProperty(winRef, 'sessionStorage', 'get').and.returnValue( - storageSpy + vi.spyOn(winRef, 'sessionStorage', 'get').mockReturnValue( + storageSpy as any ); component.ngOnInit(); expect(storageSpy.removeItem).toHaveBeenCalledWith( @@ -157,7 +157,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should patch user id', () => { - spyOnProperty(winRef, 'nativeWindow', 'get').and.returnValue({ + vi.spyOn(winRef, 'nativeWindow', 'get').mockReturnValue({ history: { state: { newUid: verificationTokenCreation.loginId } }, } as Window); component.isUpdating$.subscribe().unsubscribe(); @@ -175,7 +175,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should request email', () => { - spyOn(service, 'createVerificationToken').and.returnValue( + vi.spyOn(service, 'createVerificationToken').mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -188,13 +188,13 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should reset the form', () => { - spyOn(service, 'createVerificationToken').and.returnValue( + vi.spyOn(service, 'createVerificationToken').mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', }) ); - spyOn(component.form, 'reset').and.stub(); + vi.spyOn(component.form, 'reset').mockImplementation(() => {}); component.onSubmit(); expect(component.form.reset).toHaveBeenCalled(); }); @@ -209,7 +209,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should not create OTP', () => { - spyOn(service, 'createVerificationToken').and.returnValue( + vi.spyOn(service, 'createVerificationToken').mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -220,7 +220,7 @@ describe('OneTimePasswordLoginFormComponent', () => { }); it('should not reset the form', () => { - spyOn(component.form, 'reset').and.stub(); + vi.spyOn(component.form, 'reset').mockImplementation(() => {}); component.onSubmit(); expect(component.form.reset).not.toHaveBeenCalled(); }); @@ -263,14 +263,14 @@ describe('OneTimePasswordLoginFormComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); }); it('should call the service method on submit', () => { - spyOn(service, 'createVerificationToken').and.returnValue( + vi.spyOn(service, 'createVerificationToken').mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -298,7 +298,7 @@ describe('OneTimePasswordLoginFormComponent', () => { status: 400, url: 'https://localhost:9002/occ/v2/electronics-spa/users/anonymous/verificationToken?lang=en&curr=USD', }); - spyOn(service, 'createVerificationToken').and.returnValue( + vi.spyOn(service, 'createVerificationToken').mockReturnValue( throwError(() => httpErrorResponse) ); component.onSubmit(); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts index 26f4f6868d0..9a093dd5445 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-dialog.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Pipe, PipeTransform } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -46,7 +47,7 @@ describe('VerificationTokenDialogComponent', () => { launchDialogService = TestBed.inject(LaunchDialogService); - spyOn(launchDialogService, 'closeDialog').and.stub(); + vi.spyOn(launchDialogService, 'closeDialog').mockImplementation(() => {}); }); it('should create', () => { diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts index 9e7b8c4737c..530b75e37e1 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form-component.service.spec.ts @@ -1,4 +1,5 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { AuthConfigService, @@ -12,12 +13,11 @@ import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/fe import { of } from 'rxjs'; import { VerificationTokenFacade } from '../../root/facade'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; -import createSpy = jasmine.createSpy; class MockAuthService implements Partial { - otpLoginWithCredentials = createSpy().and.returnValue(of({})); - isUserLoggedIn = createSpy().and.returnValue(of(true)); - getCsrfToken = createSpy().and.returnValue( + otpLoginWithCredentials = vi.fn().mockReturnValue(of({})); + isUserLoggedIn = vi.fn().mockReturnValue(of(true)); + getCsrfToken = vi.fn().mockReturnValue( of({ headerName: 'CSFR', parameterName: '_csfr', @@ -62,14 +62,14 @@ function createForm(username: string, password: string, csrf: string) { } class MockVerificationTokenFacade implements Partial { - createVerificationToken = createSpy().and.returnValue( - of({ tokenId: 'testTokenId', expiresIn: '300' }) - ); + createVerificationToken = vi + .fn() + .mockReturnValue(of({ tokenId: 'testTokenId', expiresIn: '300' })); } class MockGlobalMessageService { - add = createSpy().and.stub(); - remove = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); + remove = vi.fn().mockImplementation(() => {}); } describe('VerificationTokenFormComponentService', () => { @@ -77,7 +77,7 @@ describe('VerificationTokenFormComponentService', () => { let authService: AuthService; let facade: VerificationTokenFacade; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, I18nTestingModule, FormErrorsModule], declarations: [], @@ -92,7 +92,7 @@ describe('VerificationTokenFormComponentService', () => { provideMockFeatureToggles({ ...mockFeatureToggles }), ], }).compileComponents(); - })); + }); beforeEach(() => { service = TestBed.inject(VerificationTokenFormComponentService); @@ -143,7 +143,7 @@ describe('VerificationTokenFormComponentService', () => { }); it('should reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.login(); expect(service.form.reset).toHaveBeenCalled(); }); @@ -163,7 +163,7 @@ describe('VerificationTokenFormComponentService', () => { }); it('should not reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.login(); expect(service.form.reset).not.toHaveBeenCalled(); }); @@ -171,7 +171,7 @@ describe('VerificationTokenFormComponentService', () => { }); describe('new flow', () => { // Reset test module to reconfigure FeatureToggles - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ @@ -190,7 +190,7 @@ describe('VerificationTokenFormComponentService', () => { }, ], }).compileComponents(); - })); + }); beforeEach(() => { service = TestBed.inject(VerificationTokenFormComponentService); @@ -217,7 +217,7 @@ describe('VerificationTokenFormComponentService', () => { it('should request email', () => { const form = createForm(tokenId, tokenCode, csrf); - const submitSpy = spyOn(form, 'submit'); + const submitSpy = vi.spyOn(form, 'submit'); service.login(form); expect(submitSpy).toHaveBeenCalledWith(); }); @@ -234,7 +234,7 @@ describe('VerificationTokenFormComponentService', () => { it('should not login', () => { const form = createForm(tokenId, tokenCode, csrf); - const submitSpy = spyOn(form, 'submit'); + const submitSpy = vi.spyOn(form, 'submit'); service.login(form); expect(submitSpy).not.toHaveBeenCalled(); }); diff --git a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts index 56752aa251e..0a373f0b407 100644 --- a/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts +++ b/feature-libs/user/account/components/verification-token-form/verification-token-form.component.spec.ts @@ -1,10 +1,11 @@ +import { vi } from 'vitest'; import { ChangeDetectorRef, DebugElement, Pipe, PipeTransform, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -35,7 +36,6 @@ import { } from '../user-account-constants'; import { VerificationTokenFormComponentService } from './verification-token-form-component.service'; import { VerificationTokenFormComponent } from './verification-token-form.component'; -import createSpy = jasmine.createSpy; const isBusySubject = new BehaviorSubject(false); @@ -56,15 +56,15 @@ class MockFormComponentService tokenCode: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - login = createSpy().and.stub(); - createVerificationToken = createSpy().and.returnValue( - of({ tokenId: 'testTokenId', expiresIn: '300' }) - ); - displayMessage = createSpy('displayMessage').and.stub(); + login = vi.fn().mockImplementation(() => {}); + createVerificationToken = vi + .fn() + .mockReturnValue(of({ tokenId: 'testTokenId', expiresIn: '300' })); + displayMessage = vi.fn('displayMessage').mockImplementation(() => {}); } class MockRoutingService { - go = createSpy(); + go = vi.fn(); } @Pipe({ name: 'cxUrl' }) @@ -73,7 +73,7 @@ class MockUrlPipe implements PipeTransform { } class MockLaunchDialogService implements Partial { - openDialogAndSubscribe = createSpy().and.stub(); + openDialogAndSubscribe = vi.fn().mockImplementation(() => {}); } describe('VerificationTokenFormComponent', () => { @@ -85,7 +85,7 @@ describe('VerificationTokenFormComponent', () => { let routineservice: RoutingService; let winRef: WindowRef; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -121,7 +121,7 @@ describe('VerificationTokenFormComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(VerificationTokenFormComponent); @@ -199,7 +199,7 @@ describe('VerificationTokenFormComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); @@ -227,7 +227,7 @@ describe('VerificationTokenFormComponent', () => { it('should resend OTP', () => { component.target = 'example@example.com'; component.password = 'password'; - spyOn(component, 'startWaitTimeInterval'); + vi.spyOn(component, 'startWaitTimeInterval'); component.resendOTP(); @@ -276,13 +276,13 @@ describe('VerificationTokenFormComponent', () => { it('should navigate to login and save loginId to sessionStorage', () => { component.target = 'user@example.com'; component.password = 'myPass'; - const storageSpy = jasmine.createSpyObj('Storage', [ - 'getItem', - 'setItem', - 'removeItem', - ]); - spyOnProperty(winRef, 'sessionStorage', 'get').and.returnValue( - storageSpy + const storageSpy = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + }; + vi.spyOn(winRef, 'sessionStorage', 'get').mockReturnValue( + storageSpy as any ); component.goBack(); diff --git a/feature-libs/user/account/core/connectors/user-account.connector.spec.ts b/feature-libs/user/account/core/connectors/user-account.connector.spec.ts index e0044b4cecb..cdad4000335 100644 --- a/feature-libs/user/account/core/connectors/user-account.connector.spec.ts +++ b/feature-libs/user/account/core/connectors/user-account.connector.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { VerificationToken, VerificationTokenCreation } from '../../root/model'; import { UserAccountAdapter } from './user-account.adapter'; import { UserAccountConnector } from './user-account.connector'; -import createSpy = jasmine.createSpy; const verificationTokenCreation: VerificationTokenCreation = { purpose: 'LOGIN', @@ -17,10 +17,10 @@ const verificationToken: VerificationToken = { }; class MockUserAdapter implements UserAccountAdapter { - createVerificationToken = createSpy('createVerificationToken').and.callFake( - () => of(verificationToken) - ); - load = createSpy('load').and.callFake((userId) => of(`load-${userId}`)); + createVerificationToken = vi + .fn('createVerificationToken') + .mockImplementation(() => of(verificationToken)); + load = vi.fn('load').mockImplementation((userId) => of(`load-${userId}`)); } describe('UserConnector', () => { diff --git a/feature-libs/user/account/core/facade/user-account.service.spec.ts b/feature-libs/user/account/core/facade/user-account.service.spec.ts index ad6d9ee344e..1be1c0acf3d 100644 --- a/feature-libs/user/account/core/facade/user-account.service.spec.ts +++ b/feature-libs/user/account/core/facade/user-account.service.spec.ts @@ -1,17 +1,17 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { OCC_USER_ID_CURRENT, UserIdService } from '@spartacus/core'; import { User } from '@spartacus/user/account/root'; import { of } from 'rxjs'; import { UserAccountService } from './user-account.service'; import { UserAccountConnector } from '@spartacus/user/account/core'; -import createSpy = jasmine.createSpy; class MockUserIdService implements Partial { - takeUserId = createSpy().and.returnValue(of(OCC_USER_ID_CURRENT)); + takeUserId = vi.fn().mockReturnValue(of(OCC_USER_ID_CURRENT)); } class MockUserAccountConnector implements Partial { - get = createSpy().and.callFake((uid: string) => + get = vi.fn().mockImplementation((uid: string) => of({ uid, }) diff --git a/feature-libs/user/account/core/facade/verification-token.service.spec.ts b/feature-libs/user/account/core/facade/verification-token.service.spec.ts index bc03adc282e..0c484baf242 100644 --- a/feature-libs/user/account/core/facade/verification-token.service.spec.ts +++ b/feature-libs/user/account/core/facade/verification-token.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { CommandService } from '@spartacus/core'; import { @@ -9,7 +10,6 @@ import { VerificationTokenCreation, } from '@spartacus/user/account/root'; import { of } from 'rxjs'; -import createSpy = jasmine.createSpy; const verificationTokenCreation: VerificationTokenCreation = { purpose: 'LOGIN', @@ -23,9 +23,9 @@ const verificationToken: VerificationToken = { }; class MockUserAccountConnector implements Partial { - createVerificationToken = createSpy().and.callFake(() => - of(verificationToken) - ); + createVerificationToken = vi + .fn() + .mockImplementation(() => of(verificationToken)); } describe('VerificationTokenService', () => { diff --git a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts index 570fb6e26d4..50f2768f230 100644 --- a/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts +++ b/feature-libs/user/account/occ/adapters/occ-user-account.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -100,10 +101,10 @@ describe('OccUserAccountAdapter', () => { httpMock = TestBed.inject(HttpTestingController); converter = TestBed.inject(ConverterService); occEndpointsService = TestBed.inject(OccEndpointsService); - spyOn(converter, 'pipeableMany').and.callThrough(); - spyOn(converter, 'pipeable').and.callThrough(); - spyOn(converter, 'convert').and.callThrough(); - spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converter, 'pipeableMany'); + vi.spyOn(converter, 'pipeable'); + vi.spyOn(converter, 'convert'); + vi.spyOn(occEndpointsService, 'buildUrl'); }); afterEach(() => { diff --git a/feature-libs/user/account/root/events/user-account-event.listener.spec.ts b/feature-libs/user/account/root/events/user-account-event.listener.spec.ts index 66e8af6b607..071f8f3a914 100644 --- a/feature-libs/user/account/root/events/user-account-event.listener.spec.ts +++ b/feature-libs/user/account/root/events/user-account-event.listener.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { CxEvent, @@ -8,17 +9,16 @@ import { } from '@spartacus/core'; import { Subject } from 'rxjs'; import { UserAccountEventListener } from './user-account-event.listener'; -import createSpy = jasmine.createSpy; const mockEventStream$ = new Subject(); class MockEventService implements Partial { - get = createSpy().and.returnValue(mockEventStream$.asObservable()); - dispatch = createSpy(); + get = vi.fn().mockReturnValue(mockEventStream$.asObservable()); + dispatch = vi.fn(); } class MockGlobalMessageService implements Partial { - add = createSpy(); + add = vi.fn(); } describe(`UserAccountEventListener`, () => { diff --git a/feature-libs/user/account/root/services/user-login-currency.service.spec.ts b/feature-libs/user/account/root/services/user-login-currency.service.spec.ts index 78f057e13a5..86851dca60d 100644 --- a/feature-libs/user/account/root/services/user-login-currency.service.spec.ts +++ b/feature-libs/user/account/root/services/user-login-currency.service.spec.ts @@ -14,6 +14,7 @@ import { } from '@spartacus/core'; import { Subject, of } from 'rxjs'; import { filter } from 'rxjs/operators'; +import { vi } from 'vitest'; import { UserAccountConfig } from '../config/user-account-config'; import { UserAccountFacade } from '../facade/user-account.facade'; import { @@ -21,23 +22,26 @@ import { UserLoginCurrencyPersistenceService, } from './user-login-currency-persistence.service'; import { UserLoginCurrencyService } from './user-login-currency.service'; -import createSpy = jasmine.createSpy; const mockEventStream$ = new Subject(); class MockEventService implements Partial { - get = createSpy().and.callFake((eventType: any) => - mockEventStream$.asObservable().pipe(filter((e) => e instanceof eventType)) - ); + get = vi + .fn() + .mockImplementation((eventType: any) => + mockEventStream$ + .asObservable() + .pipe(filter((e) => e instanceof eventType)) + ); } class MockCurrencyService implements Partial { - getActive = createSpy().and.returnValue(of('USD')); - setActive = createSpy(); + getActive = vi.fn().mockReturnValue(of('USD')); + setActive = vi.fn(); } class MockUserAccountFacade implements Partial { - get = createSpy().and.returnValue( + get = vi.fn().mockReturnValue( of({ currency: { isocode: 'EUR', name: 'Euro', active: true, symbol: '€' }, }) @@ -49,14 +53,14 @@ const mockStorage: { [key: string]: string | undefined } = {}; class MockUserLoginCurrencyPersistenceService implements Partial { - savePreLoginCurrency = createSpy().and.callFake((isocode: string) => { + savePreLoginCurrency = vi.fn().mockImplementation((isocode: string) => { mockStorage[PRE_LOGIN_CURRENCY_STORAGE_KEY] = JSON.stringify(isocode); }); - getPreLoginCurrency = createSpy().and.callFake(() => { + getPreLoginCurrency = vi.fn().mockImplementation(() => { const raw = mockStorage[PRE_LOGIN_CURRENCY_STORAGE_KEY]; return raw ? (JSON.parse(raw) as string) : null; }); - clearPreLoginCurrency = createSpy().and.callFake(() => { + clearPreLoginCurrency = vi.fn().mockImplementation(() => { delete mockStorage[PRE_LOGIN_CURRENCY_STORAGE_KEY]; }); } @@ -128,7 +132,7 @@ describe('UserLoginCurrencyService', () => { }); it('should not call setActive when OCC user has no currency', () => { - (userAccountFacade.get as jasmine.Spy).and.returnValue( + vi.mocked(userAccountFacade.get).mockReturnValue( of({ currency: undefined }) ); @@ -138,7 +142,7 @@ describe('UserLoginCurrencyService', () => { }); it('should not call setActive when OCC user currency has no isocode', () => { - (userAccountFacade.get as jasmine.Spy).and.returnValue( + vi.mocked(userAccountFacade.get).mockReturnValue( of({ currency: { name: 'Euro' } }) ); @@ -148,7 +152,7 @@ describe('UserLoginCurrencyService', () => { }); it('should not call setActive when OCC currency matches pre-login currency', () => { - (userAccountFacade.get as jasmine.Spy).and.returnValue( + vi.mocked(userAccountFacade.get).mockReturnValue( of({ currency: { isocode: 'USD' } }) ); @@ -161,9 +165,9 @@ describe('UserLoginCurrencyService', () => { describe('on LogoutEvent', () => { it('should restore pre-login currency and clear storage', () => { mockStorage[PRE_LOGIN_CURRENCY_STORAGE_KEY] = JSON.stringify('GBP'); - ( - currencyPersistence.getPreLoginCurrency as jasmine.Spy - ).and.returnValue('GBP'); + vi.mocked(currencyPersistence.getPreLoginCurrency).mockReturnValue( + 'GBP' + ); mockEventStream$.next(new LogoutEvent()); diff --git a/feature-libs/user/karma.conf.js b/feature-libs/user/karma.conf.js deleted file mode 100644 index 8d375c20da3..00000000000 --- a/feature-libs/user/karma.conf.js +++ /dev/null @@ -1,52 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-coverage'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - require('karma-junit-reporter'), - ], - client: { - clearContext: true, // close Jasmine Spec Runner output in browser to avoid 'Some of your tests did a full page reload!' error when '--no-watch' is active - jasmine: { - random: false, - }, - }, - reporters: ['progress', 'kjhtml', 'dots', 'junit'], - junitReporter: { - outputFile: 'unit-test-user.xml', - outputDir: require('path').join(__dirname, '../../unit-tests-reports'), - useBrowserName: false, - }, - coverageReporter: { - dir: require('path').join(__dirname, '../../coverage/user'), - reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }], - check: { - global: { - statements: 90, - lines: 90, - branches: 75, - functions: 80, - }, - }, - }, - captureTimeout: 210000, - browserDisconnectTolerance: 3, - browserDisconnectTimeout: 210000, - browserNoActivityTimeout: 210000, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false, - restartOnFileChange: true, - }); -}; diff --git a/feature-libs/user/profile/components/address-book/address-book.component.service.spec.ts b/feature-libs/user/profile/components/address-book/address-book.component.service.spec.ts index cd12deab44d..c9ee8465b23 100644 --- a/feature-libs/user/profile/components/address-book/address-book.component.service.spec.ts +++ b/feature-libs/user/profile/components/address-book/address-book.component.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { Address, User, UserAddressService } from '@spartacus/core'; import { Observable, of } from 'rxjs'; @@ -22,11 +23,11 @@ const mockUser: User = { }; class MockUserAddressService { - loadAddresses = jasmine.createSpy(); - addUserAddress = jasmine.createSpy(); - updateUserAddress = jasmine.createSpy(); - setAddressAsDefault = jasmine.createSpy(); - deleteUserAddress = jasmine.createSpy(); + loadAddresses = vi.fn(); + addUserAddress = vi.fn(); + updateUserAddress = vi.fn(); + setAddressAsDefault = vi.fn(); + deleteUserAddress = vi.fn(); getAddresses(): Observable { return of(mockAddresses); diff --git a/feature-libs/user/profile/components/address-book/address-book.component.spec.ts b/feature-libs/user/profile/components/address-book/address-book.component.spec.ts index 3b9d69acdeb..d065e419f1d 100644 --- a/feature-libs/user/profile/components/address-book/address-book.component.spec.ts +++ b/feature-libs/user/profile/components/address-book/address-book.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { Component, DebugElement, @@ -5,7 +6,7 @@ import { Input, Output, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Address, @@ -23,14 +24,14 @@ import { } from '@spartacus/core'; import { CardModule, SpinnerModule } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; -import { BehaviorSubject, Observable, of } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Observable, of } from 'rxjs'; import { AddressFormComponent } from '../public_api'; import { AddressBookComponent } from './address-book.component'; import { AddressBookComponentService } from './address-book.component.service'; import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/feature-toggles/testing'; class MockGlobalMessageService { - add = jasmine.createSpy(); + add = vi.fn(); } class MockLanguageService { @@ -65,11 +66,11 @@ const isLoading = new BehaviorSubject(false); const isError = new BehaviorSubject(false); class MockComponentService { - loadAddresses = jasmine.createSpy(); - addUserAddress = jasmine.createSpy(); - updateUserAddress = jasmine.createSpy(); - deleteUserAddress = jasmine.createSpy(); - setAddressAsDefault = jasmine.createSpy(); + loadAddresses = vi.fn(); + addUserAddress = vi.fn(); + updateUserAddress = vi.fn(); + deleteUserAddress = vi.fn(); + setAddressAsDefault = vi.fn(); getAddressesStateLoading(): Observable { return isLoading.asObservable(); } @@ -121,7 +122,7 @@ describe('AddressBookComponent', () => { let el: DebugElement; let addressBookComponentService: AddressBookComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [SpinnerModule, CardModule, AddressBookComponent], providers: [ @@ -141,34 +142,34 @@ describe('AddressBookComponent', () => { }, }, ], - }) - .overrideComponent(AddressBookComponent, { - remove: { - imports: [ - TranslatePipe, - CxDatePipe, - AddressFormComponent, - FeatureDirective, - ], - }, - add: { - imports: [ - MockTranslatePipe, - MockDatePipe, - MockAddressFormComponent, - MockFeatureDirective, - ], - }, - }) - .compileComponents(); - })); + }).overrideComponent(AddressBookComponent, { + remove: { + imports: [ + TranslatePipe, + CxDatePipe, + AddressFormComponent, + FeatureDirective, + ], + }, + add: { + imports: [ + MockTranslatePipe, + MockDatePipe, + MockAddressFormComponent, + MockFeatureDirective, + ], + }, + }); + await TestBed.compileComponents(); + }); beforeEach(() => { fixture = TestBed.createComponent(AddressBookComponent); component = fixture.componentInstance; - spyOn(component, 'addAddressButtonHandle'); + vi.spyOn(component, 'addAddressButtonHandle'); el = fixture.debugElement; addressBookComponentService = TestBed.inject(AddressBookComponentService); + TestBed.inject(FeatureToggles).enableHierarchicalAddressFormat = true; isLoading.next(false); component.ngOnInit(); @@ -205,35 +206,35 @@ describe('AddressBookComponent', () => { }); it('should call editAddressButtonHandle(address: Address)', () => { - spyOn(component, 'editAddressButtonHandle'); + vi.spyOn(component, 'editAddressButtonHandle'); component.editAddressButtonHandle(mockAddress); expect(component.editAddressButtonHandle).toHaveBeenCalledWith(mockAddress); }); it('should call addAddressSubmit(address: Address)', () => { - spyOn(component, 'addAddressSubmit'); + vi.spyOn(component, 'addAddressSubmit'); component.addAddressSubmit(mockAddress); expect(component.addAddressSubmit).toHaveBeenCalledWith(mockAddress); }); it('should call addAddressCancel()', () => { - spyOn(component, 'addAddressCancel'); + vi.spyOn(component, 'addAddressCancel'); component.addAddressCancel(); expect(component.addAddressCancel).toHaveBeenCalledWith(); }); it('should call editAddressSubmit(address: Address)', () => { - spyOn(component, 'editAddressSubmit'); + vi.spyOn(component, 'editAddressSubmit').mockImplementation(() => {}); component.editAddressSubmit(mockAddress); expect(component.editAddressSubmit).toHaveBeenCalledWith(mockAddress); }); it('should call editAddressCancel()', () => { - spyOn(component, 'editAddressCancel'); + vi.spyOn(component, 'editAddressCancel'); component.editAddressCancel(); expect(component.editAddressCancel).toHaveBeenCalledWith(); @@ -252,9 +253,10 @@ describe('AddressBookComponent', () => { ); }); - it('should display default label on address default', () => { + it('should display default label on address default', async () => { mockAddress.defaultAddress = true; - fixture.detectChanges(); + fixture.componentRef.changeDetectorRef.detectChanges(); + await fixture.whenStable(); const element = el.query(By.css('.card-header')); expect(element.nativeElement.textContent).toContain( ' ✓ addressCard.default ' @@ -277,7 +279,7 @@ describe('AddressBookComponent', () => { }); it('should handle edit on card', () => { - spyOn(component, 'deleteAddress'); + vi.spyOn(component, 'deleteAddress'); component.setEdit(mockAddress.id || '1'); expect(component.editCard).toEqual(mockAddress.id); @@ -298,11 +300,21 @@ describe('AddressBookComponent', () => { beforeEach(() => { isLoading.next(false); isError.next(false); - spyOn( + vi.spyOn( addressBookComponentService, 'getAddressesStateLoading' - ).and.callThrough(); - spyOn(addressBookComponentService, 'getAddressesError').and.callThrough(); + ).mockReturnValue(isLoading.asObservable()); + vi.spyOn( + addressBookComponentService, + 'getAddressesError' + ).mockReturnValue(isError.asObservable()); + ( + addressBookComponentService.addUserAddress as ReturnType + ).mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it('should close the form when addUserAddress succeeds', () => { @@ -354,11 +366,23 @@ describe('AddressBookComponent', () => { beforeEach(() => { isLoading.next(false); isError.next(false); - spyOn( + vi.spyOn( addressBookComponentService, 'getAddressesStateLoading' - ).and.callThrough(); - spyOn(addressBookComponentService, 'getAddressesError').and.callThrough(); + ).mockReturnValue(isLoading.asObservable()); + vi.spyOn( + addressBookComponentService, + 'getAddressesError' + ).mockReturnValue(isError.asObservable()); + ( + addressBookComponentService.updateUserAddress as ReturnType< + typeof vi.fn + > + ).mockClear(); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it('should close the form when updateUserAddress succeeds', () => { @@ -396,16 +420,22 @@ describe('AddressBookComponent', () => { }); describe('getCardContent', () => { - it('should use city name and country name when available', () => { + it('should use city name and country name when available', async () => { + (component as any).hierarchicalAddressConfig = { + hierarchicalAddress: { + countriesUsingHierarchicalAddressFormat: ['CN'], + }, + }; const addressWithNames: Address = { ...mockAddress, city: { name: 'Beijing', isocode: 'CN-11-1' }, country: { name: 'China', isocode: 'CN' }, region: { name: 'Beijing Region', isocode: 'CN-11' }, }; - let card: any; - component.getCardContent(addressWithNames).subscribe((c) => (card = c)); - expect(card.text.some((t: string) => t.includes('Beijing'))).toBe(true); + const card = await firstValueFrom( + component.getCardContent(addressWithNames) + ); + expect(card.text?.some((t: string) => t.includes('Beijing'))).toBe(true); }); it('should use legacy region+country format when toggle is off', () => { @@ -475,17 +505,17 @@ describe('AddressBookComponent', () => { it('should set correct header for add new address', () => { component.showEditAddressForm = false; component.showAddAddressForm = true; - fixture.detectChanges(); + fixture.componentRef.changeDetectorRef.detectChanges(); - expect(el.query(By.css('h2')).nativeElement.innerText).toEqual( + expect(el.query(By.css('h2')).nativeElement.textContent?.trim()).toEqual( 'addressBook.addNewDeliveryAddress' ); }); it('should set correct header for edit address', () => { component.editAddressButtonHandle(mockAddress); - fixture.detectChanges(); + fixture.componentRef.changeDetectorRef.detectChanges(); - expect(el.query(By.css('h2')).nativeElement.innerText).toEqual( + expect(el.query(By.css('h2')).nativeElement.textContent?.trim()).toEqual( 'addressBook.editDeliveryAddress' ); }); diff --git a/feature-libs/user/profile/components/address-book/address-book.component.ts b/feature-libs/user/profile/components/address-book/address-book.component.ts index 519ea56ccc7..121d27ad031 100644 --- a/feature-libs/user/profile/components/address-book/address-book.component.ts +++ b/feature-libs/user/profile/components/address-book/address-book.component.ts @@ -11,10 +11,10 @@ import { FeatureToggles, GlobalMessageService, GlobalMessageType, - HierarchicalAddressConfig, LanguageService, TranslatePipe, TranslationService, + HierarchicalAddressConfig, } from '@spartacus/core'; import { Card, diff --git a/feature-libs/user/profile/components/address-book/address-form/address-form.component.spec.ts b/feature-libs/user/profile/components/address-book/address-form/address-form.component.spec.ts index 79518dc66d9..39d6a4d5726 100644 --- a/feature-libs/user/profile/components/address-book/address-form/address-form.component.spec.ts +++ b/feature-libs/user/profile/components/address-book/address-form/address-form.component.spec.ts @@ -1,10 +1,11 @@ +import { vi } from 'vitest'; import { ChangeDetectionStrategy, DebugElement, Directive, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -12,6 +13,7 @@ import { Address, AddressValidation, Country, + FeatureDirective, FeatureToggles, GlobalMessageService, HierarchicalAddressConfig, @@ -31,11 +33,10 @@ import { provideMockFeatureToggles, } from 'core-libs/core/src/features-config/feature-toggles/testing'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; -import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, firstValueFrom, of } from 'rxjs'; import { take } from 'rxjs/operators'; import { UserProfileFacade } from '../../../root/facade/user-profile.facade'; import { AddressFormComponent } from './address-form.component'; -import createSpy = jasmine.createSpy; const mockTitles: Title[] = [ { @@ -161,9 +162,9 @@ describe('AddressFormComponent', () => { const defaultAddressCheckbox = (): DebugElement => fixture.debugElement.query(By.css('[formcontrolname=defaultAddress]')); - beforeEach(waitForAsync(() => { + beforeEach(async () => { mockGlobalMessageService = { - add: createSpy(), + add: vi.fn(), }; TestBed.configureTestingModule({ @@ -174,7 +175,6 @@ describe('AddressFormComponent', () => { FormErrorsModule, AddressFormComponent, MockNgSelectA11yDirective, - MockFeatureDirective, ], providers: [ { provide: LaunchDialogService, useClass: MockLaunchDialogService }, @@ -197,14 +197,20 @@ describe('AddressFormComponent', () => { ], }) .overrideComponent(AddressFormComponent, { - set: { changeDetection: ChangeDetectionStrategy.Eager }, + add: { + changeDetection: ChangeDetectionStrategy.Eager, + imports: [MockFeatureDirective], + }, + remove: { + imports: [FeatureDirective], + }, }) .compileComponents(); userProfileFacade = TestBed.inject(UserProfileFacade); userAddressService = TestBed.inject(UserAddressService); launchDialogService = TestBed.inject(LaunchDialogService); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(AddressFormComponent); @@ -212,38 +218,38 @@ describe('AddressFormComponent', () => { controls = component.addressForm.controls; component.showTitleCode = true; - spyOn(component.submitAddress, 'emit').and.callThrough(); - spyOn(component.backToAddress, 'emit').and.callThrough(); + vi.spyOn(component.submitAddress, 'emit'); + vi.spyOn(component.backToAddress, 'emit'); }); it('should be created', () => { expect(component).toBeTruthy(); }); - it('should call ngOnInit to get countries data even when they not exist', (done) => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userAddressService, 'loadDeliveryCountries').and.stub(); + it('should call ngOnInit to get countries data even when they not exist', async () => { + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userAddressService, 'loadDeliveryCountries').mockImplementation( + () => {} + ); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); - spyOn(userAddressService, 'getAddresses').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getAddresses').mockReturnValue(of([])); component.ngOnInit(); - component.countries$ - .subscribe(() => { - expect(userAddressService.loadDeliveryCountries).toHaveBeenCalled(); - done(); - }) - .unsubscribe(); + await firstValueFrom(component.countries$); + expect(userAddressService.loadDeliveryCountries).toHaveBeenCalled(); }); it('should call ngOnInit to get countries, titles and regions data when data exist', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue( + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( of(mockCountries) ); - spyOn(userProfileFacade, 'getTitles').and.returnValue(of(mockTitles)); - spyOn(userAddressService, 'getRegions').and.returnValue(of(mockRegions)); + vi.spyOn(userProfileFacade, 'getTitles').mockReturnValue(of(mockTitles)); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of(mockRegions)); component.ngOnInit(); @@ -272,15 +278,17 @@ describe('AddressFormComponent', () => { }); it('should add address with address verification result "accept"', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userProfileFacade, 'getTitles').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userProfileFacade, 'getTitles').mockReturnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); const mockAddressVerificationResult: AddressValidation = { decision: 'ACCEPT', }; - spyOn(component, 'openSuggestedAddress'); + vi.spyOn(component, 'openSuggestedAddress'); component.ngOnInit(); component['handleAddressVerificationResults']( mockAddressVerificationResult @@ -291,9 +299,11 @@ describe('AddressFormComponent', () => { }); it('should display error message on address verification result "reject"', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userProfileFacade, 'getTitles').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userProfileFacade, 'getTitles').mockReturnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); const mockAddressVerificationResult: AddressValidation = { decision: 'REJECT', @@ -305,7 +315,7 @@ describe('AddressFormComponent', () => { mockAddressVerificationResult ); - spyOn(component, 'openSuggestedAddress'); + vi.spyOn(component, 'openSuggestedAddress'); component.ngOnInit(); if (mockAddressVerificationResult.errors) { mockAddressVerificationResult.errors.errors = [{ subject: 'titleCode' }]; @@ -315,16 +325,18 @@ describe('AddressFormComponent', () => { }); it('should open suggested address dialog with address verification result "review"', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userProfileFacade, 'getTitles').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userProfileFacade, 'getTitles').mockReturnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); const mockAddressVerificationResult: AddressValidation = { decision: 'REVIEW', }; - spyOn(component, 'openSuggestedAddress').and.callThrough(); - spyOn(launchDialogService, 'openDialogAndSubscribe'); + vi.spyOn(component, 'openSuggestedAddress'); + vi.spyOn(launchDialogService, 'openDialogAndSubscribe'); component.ngOnInit(); component['handleAddressVerificationResults']( @@ -337,7 +349,7 @@ describe('AddressFormComponent', () => { }); it('should emit submitAddress if dialog was closed with selected address as parameter', () => { - spyOn(launchDialogService, 'openDialogAndSubscribe'); + vi.spyOn(launchDialogService, 'openDialogAndSubscribe'); const mockAddressVerificationResult: AddressValidation = { decision: 'REVIEW', }; @@ -353,7 +365,7 @@ describe('AddressFormComponent', () => { }); it('should call verifyAddress() when address has some changes', () => { - spyOn(userAddressService, 'verifyAddress').and.returnValue( + vi.spyOn(userAddressService, 'verifyAddress').mockReturnValue( of({ decision: 'ACCEPT', }) @@ -367,7 +379,7 @@ describe('AddressFormComponent', () => { }); it('should not call verifyAddress() when address does not have change', () => { - spyOn(userAddressService, 'verifyAddress').and.stub(); + vi.spyOn(userAddressService, 'verifyAddress').mockImplementation(() => {}); component.ngOnInit(); component.addressForm.setValue(mockAddress); component.verifyAddress(); @@ -381,7 +393,7 @@ describe('AddressFormComponent', () => { it('should toggleDefaultAddress() adapt control value', () => { component.setAsDefaultField = true; - spyOn(userAddressService, 'getAddresses').and.returnValue( + vi.spyOn(userAddressService, 'getAddresses').mockReturnValue( of([mockAddress]) ); @@ -393,7 +405,7 @@ describe('AddressFormComponent', () => { }); it('should call countrySelected()', () => { - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); const mockCountryIsocode = 'test country isocode'; component.countrySelected({ isocode: mockCountryIsocode }); component.ngOnInit(); @@ -409,7 +421,13 @@ describe('AddressFormComponent', () => { }); it('should set isHierarchicalAddressFormat and add validators when CN is selected', () => { - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); + (component as any).featureToggles = { + enableHierarchicalAddressFormat: true, + }; + (component as any).hierarchicalAddressConfig = { + hierarchicalAddress: { countriesUsingHierarchicalAddressFormat: ['CN'] }, + }; component.countrySelected({ isocode: 'CN' }); expect(component.isHierarchicalAddressFormat).toBe(true); expect(component.addressForm.get('cellphone')?.validator).toBeTruthy(); @@ -417,7 +435,7 @@ describe('AddressFormComponent', () => { }); it('should clear validators and reset state when switching away from CN', () => { - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); component.countrySelected({ isocode: 'CN' }); component.countrySelected({ isocode: 'US' }); expect(component.isHierarchicalAddressFormat).toBe(false); @@ -426,6 +444,9 @@ describe('AddressFormComponent', () => { }); it('should reset town and district when region changes for CN address', () => { + (component as any).featureToggles = { + enableHierarchicalAddressFormat: true, + }; component.isHierarchicalAddressFormat = true; component.addressForm.get('town')?.setValue('old-town'); component.addressForm.get('district')?.setValue('old-district'); @@ -456,35 +477,43 @@ describe('AddressFormComponent', () => { }); it('should initialize cities as empty array', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); component.ngOnInit(); expect(component.cities).toEqual([]); }); it('should initialize districts as empty array', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); component.ngOnInit(); expect(component.districts).toEqual([]); }); it('should have empty cities when no region is selected', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); component.ngOnInit(); expect(component.cities).toEqual([]); }); it('should have empty districts when no city is selected', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); component.ngOnInit(); expect(component.districts).toEqual([]); }); it('should call verifyAddress', () => { - spyOn(component, 'verifyAddress').and.callThrough(); + vi.spyOn(component, 'verifyAddress'); const mockCountryIsocode = 'test country isocode'; component.regionSelected({ isocode: mockCountryIsocode }); component.ngOnInit(); @@ -502,10 +531,12 @@ describe('AddressFormComponent', () => { fixture.debugElement.query(By.css('.btn-primary')); it('should call "verifyAddress" function when being clicked and when form is valid', () => { - spyOn(userAddressService, 'getDeliveryCountries').and.returnValue(of([])); - spyOn(userProfileFacade, 'getTitles').and.returnValue(of([])); - spyOn(userAddressService, 'getRegions').and.returnValue(of([])); - spyOn(component, 'verifyAddress'); + vi.spyOn(userAddressService, 'getDeliveryCountries').mockReturnValue( + of([]) + ); + vi.spyOn(userProfileFacade, 'getTitles').mockReturnValue(of([])); + vi.spyOn(userAddressService, 'getRegions').mockReturnValue(of([])); + vi.spyOn(component, 'verifyAddress'); fixture.detectChanges(); @@ -545,7 +576,9 @@ describe('AddressFormComponent', () => { fixture.detectChanges(); expect( // eslint-disable-next-line no-restricted-syntax - fixture.nativeElement.querySelector('.btn-secondary').innerText + fixture.nativeElement + .querySelector('.btn-secondary') + .textContent?.trim() ).toEqual('Back to cart'); }); @@ -554,7 +587,9 @@ describe('AddressFormComponent', () => { fixture.detectChanges(); expect( // eslint-disable-next-line no-restricted-syntax - fixture.nativeElement.querySelector('.btn-secondary').innerText + fixture.nativeElement + .querySelector('.btn-secondary') + .textContent?.trim() ).toEqual('addressForm.chooseAddress'); }); }); @@ -582,7 +617,7 @@ describe('AddressFormComponent', () => { it('should call "back" function after being clicked', () => { fixture.detectChanges(); - spyOn(component, 'back'); + vi.spyOn(component, 'back'); // eslint-disable-next-line no-restricted-syntax getBackBtn().nativeElement.click(); expect(component.back).toHaveBeenCalled(); @@ -590,13 +625,13 @@ describe('AddressFormComponent', () => { }); it('should unsubscribe from any subscriptions when destroyed', () => { - spyOn(component.subscription, 'unsubscribe'); + vi.spyOn(component.subscription, 'unsubscribe'); component.ngOnDestroy(); expect(component.subscription.unsubscribe).toHaveBeenCalled(); }); it('should show the "Set as default" checkbox when there is one or more saved addresses', () => { - spyOn(userAddressService, 'getAddresses').and.returnValue( + vi.spyOn(userAddressService, 'getAddresses').mockReturnValue( of([mockAddress]) ); @@ -606,7 +641,7 @@ describe('AddressFormComponent', () => { }); it('should not show the "Set as default" checkbox when there no saved addresses', () => { - spyOn(userAddressService, 'getAddresses').and.returnValue(of([])); + vi.spyOn(userAddressService, 'getAddresses').mockReturnValue(of([])); fixture.detectChanges(); @@ -628,7 +663,7 @@ describe('AddressFormComponent', () => { }); it('verifyAddress should call OCC verifyAddress when toggle is off', () => { - spyOn(userAddressService, 'verifyAddress').and.returnValue( + vi.spyOn(userAddressService, 'verifyAddress').mockReturnValue( of({ decision: 'ACCEPT' }) ); component.ngOnInit(); @@ -646,29 +681,60 @@ describe('AddressFormComponent', () => { expect(component.isHierarchicalAddressFormat).toBe(false); }); }); +}); - describe('a11yAddressFormInitialFocus', () => { - let featureTogglesController: MockFeatureTogglesController; +describe('AddressFormComponent - a11yAddressFormInitialFocus', () => { + let fixture: ComponentFixture; - const getFocusForm = (): DebugElement => - fixture.debugElement.query(By.directive(FocusDirective)); + const getFocusForm = (): DebugElement => + fixture.debugElement.query(By.directive(FocusDirective)); - beforeEach(() => { - featureTogglesController = TestBed.inject(MockFeatureTogglesController); + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + NgSelectModule, + I18nTestingModule, + FormErrorsModule, + AddressFormComponent, + MockNgSelectA11yDirective, + ], + providers: [ + { provide: LaunchDialogService, useClass: MockLaunchDialogService }, + { provide: UserAddressService, useClass: MockUserAddressService }, + { provide: GlobalMessageService, useValue: { add: vi.fn() } }, + { provide: UserProfileFacade, useClass: MockUserProfileFacade }, + { provide: LanguageService, useClass: MockLanguageService }, + provideMockFeatureToggles({ + ...mockFeatureToggles, + a11yAddressFormInitialFocus: true, + }), + { + provide: HierarchicalAddressConfig, + useValue: { + hierarchicalAddress: { + countriesUsingHierarchicalAddressFormat: ['CN'], + }, + }, + }, + ], }); - - it('should apply cxFocus to the form when a11yAddressFormInitialFocus is true', () => { - featureTogglesController.set('a11yAddressFormInitialFocus', true); - fixture.detectChanges(); - - expect(getFocusForm()).toBeTruthy(); + TestBed.overrideComponent(AddressFormComponent, { + add: { + changeDetection: ChangeDetectionStrategy.Eager, + imports: [MockFeatureDirective], + }, + remove: { imports: [FeatureDirective] }, }); + await TestBed.compileComponents(); + }); - it('should not apply cxFocus to the form when a11yAddressFormInitialFocus is false', () => { - featureTogglesController.set('a11yAddressFormInitialFocus', false); - fixture.detectChanges(); + beforeEach(() => { + fixture = TestBed.createComponent(AddressFormComponent); + fixture.detectChanges(); + }); - expect(getFocusForm()).toBeNull(); - }); + it('should apply cxFocus to the form when a11yAddressFormInitialFocus is enabled', () => { + expect(getFocusForm()).toBeTruthy(); }); }); diff --git a/feature-libs/user/profile/components/address-book/address-form/address-form.component.ts b/feature-libs/user/profile/components/address-book/address-form/address-form.component.ts index 07abe5fb0a0..18d35f7a945 100644 --- a/feature-libs/user/profile/components/address-book/address-form/address-form.component.ts +++ b/feature-libs/user/profile/components/address-book/address-form/address-form.component.ts @@ -37,13 +37,13 @@ import { FeatureToggles, GlobalMessageService, GlobalMessageType, - HierarchicalAddressConfig, LanguageService, Region, Title, TranslatePipe, TranslationService, UserAddressService, + HierarchicalAddressConfig, } from '@spartacus/core'; import { FocusDirective, diff --git a/feature-libs/user/profile/components/address-book/address-form/suggested-addresses-dialog/suggested-addresses-dialog.component.spec.ts b/feature-libs/user/profile/components/address-book/address-form/suggested-addresses-dialog/suggested-addresses-dialog.component.spec.ts index f0fd92d46d3..5d39a63f916 100644 --- a/feature-libs/user/profile/components/address-book/address-form/suggested-addresses-dialog/suggested-addresses-dialog.component.spec.ts +++ b/feature-libs/user/profile/components/address-book/address-form/suggested-addresses-dialog/suggested-addresses-dialog.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { Address, @@ -20,7 +21,6 @@ import { } from '@spartacus/storefront'; import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { SuggestedAddressDialogComponent } from './suggested-addresses-dialog.component'; -import createSpy = jasmine.createSpy; const mockData = { enteredAddress: {}, @@ -36,7 +36,7 @@ class MockCxIconComponent { } class MockLaunchDialogService implements Partial { - closeDialog = createSpy(); + closeDialog = vi.fn(); data$ = of(mockData); } @@ -47,7 +47,7 @@ describe('SuggestedAddressDialogComponent', () => { let launchDialogService: LaunchDialogService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [FormsModule, SuggestedAddressDialogComponent], providers: [ @@ -68,7 +68,7 @@ describe('SuggestedAddressDialogComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(SuggestedAddressDialogComponent); @@ -89,7 +89,7 @@ describe('SuggestedAddressDialogComponent', () => { }); it('should call setSelectedData when component constructed', () => { - spyOn(component, 'setSelectedAddress'); + vi.spyOn(component, 'setSelectedAddress'); component.data$.pipe(take(1)).subscribe((result) => { expect(result).toEqual(mockData); @@ -116,7 +116,7 @@ describe('SuggestedAddressDialogComponent', () => { it('should closeModal when user click outside', () => { const el = fixture.debugElement.nativeElement; - spyOn(component, 'closeModal'); + vi.spyOn(component, 'closeModal'); el.click(); expect(component.closeModal).toHaveBeenCalledWith('Cross click'); diff --git a/feature-libs/user/profile/components/close-account/components/close-account-modal/close-account-modal.component.spec.ts b/feature-libs/user/profile/components/close-account/components/close-account-modal/close-account-modal.component.spec.ts index df623da8108..12e694d2be0 100644 --- a/feature-libs/user/profile/components/close-account/components/close-account-modal/close-account-modal.component.spec.ts +++ b/feature-libs/user/profile/components/close-account/components/close-account-modal/close-account-modal.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { Component, Input } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AuthService, GlobalMessageService, @@ -18,14 +19,13 @@ import { import { UserProfileFacade } from '@spartacus/user/profile/root'; import { Observable, of, throwError } from 'rxjs'; import { CloseAccountModalComponent } from './close-account-modal.component'; -import createSpy = jasmine.createSpy; class MockGlobalMessageService implements Partial { - add = createSpy(); + add = vi.fn(); } class MockUserProfileFacade implements Partial { - close = createSpy().and.returnValue(of(undefined)); + close = vi.fn().mockReturnValue(of(undefined)); } class MockAuthService implements Partial { @@ -33,7 +33,7 @@ class MockAuthService implements Partial { return of(true); } - coreLogout = createSpy().and.returnValue(Promise.resolve()); + coreLogout = vi.fn().mockReturnValue(Promise.resolve()); } class MockRoutingService implements Partial { @@ -41,7 +41,7 @@ class MockRoutingService implements Partial { } class MockLaunchDialogService implements Partial { - closeDialog = createSpy(); + closeDialog = vi.fn(); } @Component({ @@ -65,7 +65,7 @@ describe('CloseAccountModalComponent', () => { let globalMessageService: GlobalMessageService; let launchDialogService: LaunchDialogService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [CloseAccountModalComponent], providers: [ @@ -106,7 +106,7 @@ describe('CloseAccountModalComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(CloseAccountModalComponent); @@ -127,8 +127,8 @@ describe('CloseAccountModalComponent', () => { }); it('should navigate away and dismiss modal when account is closed', () => { - spyOn(component, 'onSuccess').and.callThrough(); - // spyOn(launchDialogService, 'closeDialog').and.callThrough(); + vi.spyOn(component, 'onSuccess'); + // vi.spyOn(launchDialogService, 'closeDialog'); component.ngOnInit(); component.closeAccount(); @@ -139,9 +139,9 @@ describe('CloseAccountModalComponent', () => { }); it('should dismiss modal when account failed to close', () => { - spyOn(component, 'onError').and.callThrough(); - // spyOn(launchDialogService, 'closeDialog').and.callThrough(); - (userFacade.close as any).and.returnValue(throwError(() => undefined)); + vi.spyOn(component, 'onError'); + // vi.spyOn(launchDialogService, 'closeDialog'); + (userFacade.close as any).mockReturnValue(throwError(() => undefined)); component.ngOnInit(); component.closeAccount(); @@ -153,7 +153,7 @@ describe('CloseAccountModalComponent', () => { it('should closeModal when user click outside', () => { const el = fixture.debugElement.nativeElement; - spyOn(component, 'dismissModal'); + vi.spyOn(component, 'dismissModal'); el.click(); expect(component.dismissModal).toHaveBeenCalledWith('Cross click'); diff --git a/feature-libs/user/profile/components/close-account/components/close-account/close-account.component.spec.ts b/feature-libs/user/profile/components/close-account/components/close-account/close-account.component.spec.ts index b4345bade4d..309fff4a0bc 100644 --- a/feature-libs/user/profile/components/close-account/components/close-account/close-account.component.spec.ts +++ b/feature-libs/user/profile/components/close-account/components/close-account/close-account.component.spec.ts @@ -1,4 +1,5 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MockTranslatePipe, @@ -25,7 +26,7 @@ describe('CloseAccountComponent', () => { let launchDialogService: LaunchDialogService; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [CloseAccountComponent], providers: [ @@ -38,7 +39,7 @@ describe('CloseAccountComponent', () => { add: { imports: [MockTranslatePipe] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(CloseAccountComponent); @@ -53,7 +54,7 @@ describe('CloseAccountComponent', () => { }); it('should open modal', () => { - spyOn(launchDialogService, 'openDialog'); + vi.spyOn(launchDialogService, 'openDialog'); component.openModal(); @@ -65,7 +66,7 @@ describe('CloseAccountComponent', () => { }); it('should navigate to home on cancel', () => { - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); fixture.detectChanges(); const cancelBtn = fixture.debugElement.query( By.css('button.btn-secondary') diff --git a/feature-libs/user/profile/components/forgot-password/forgot-password-component.service.spec.ts b/feature-libs/user/profile/components/forgot-password/forgot-password-component.service.spec.ts index 157c4c2c3aa..caf5d6f5d8b 100644 --- a/feature-libs/user/profile/components/forgot-password/forgot-password-component.service.spec.ts +++ b/feature-libs/user/profile/components/forgot-password/forgot-password-component.service.spec.ts @@ -1,4 +1,5 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { AuthConfigService, @@ -11,13 +12,12 @@ import { FormErrorsModule } from '@spartacus/storefront'; import { UserPasswordFacade } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { ForgotPasswordComponentService } from './forgot-password-component.service'; -import createSpy = jasmine.createSpy; class MockUserPasswordService implements Partial { - requestForgotPasswordEmail = createSpy().and.returnValue(of({})); + requestForgotPasswordEmail = vi.fn().mockReturnValue(of({})); } class MockRoutingService implements Partial { - go = createSpy().and.stub(); + go = vi.fn().mockImplementation(() => {}); } class MockAuthConfigService implements Partial { @@ -26,7 +26,7 @@ class MockAuthConfigService implements Partial { } } class MockGlobalMessageService { - add = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); } describe('ForgotPasswordComponentService', () => { @@ -35,7 +35,7 @@ describe('ForgotPasswordComponentService', () => { let routingService: RoutingService; let userPasswordFacade: UserPasswordFacade; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule, I18nTestingModule, FormErrorsModule], declarations: [], @@ -47,7 +47,7 @@ describe('ForgotPasswordComponentService', () => { { provide: GlobalMessageService, useClass: MockGlobalMessageService }, ], }).compileComponents(); - })); + }); beforeEach(() => { service = TestBed.inject(ForgotPasswordComponentService); @@ -65,16 +65,16 @@ describe('ForgotPasswordComponentService', () => { service['busy$'].next(true); let result; service.isUpdating$.subscribe((value) => (result = value)).unsubscribe(); - expect(result).toBeTrue(); - expect(service.form.disabled).toBeTrue(); + expect(result).toBe(true); + expect(service.form.disabled).toBe(true); }); it('should return false', () => { service['busy$'].next(false); let result; service.isUpdating$.subscribe((value) => (result = value)).unsubscribe(); - expect(result).toBeFalse; - expect(service.form.disabled).toBeFalse(); + expect(result).toBe(false); + expect(service.form.disabled).toBe(false); }); }); @@ -99,13 +99,13 @@ describe('ForgotPasswordComponentService', () => { }); it('should reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.requestEmail(); expect(service.form.reset).toHaveBeenCalled(); }); it('should not redirect when flow different than ResourceOwnerPasswordFlow is used', () => { - spyOn(authConfigService, 'getOAuthFlow').and.returnValue( + vi.spyOn(authConfigService, 'getOAuthFlow').mockReturnValue( OAuthFlow.ImplicitFlow ); service.requestEmail(); @@ -133,7 +133,7 @@ describe('ForgotPasswordComponentService', () => { }); it('should not reset the form', () => { - spyOn(service.form, 'reset').and.stub(); + vi.spyOn(service.form, 'reset').mockImplementation(() => {}); service.requestEmail(); expect(service.form.reset).not.toHaveBeenCalled(); }); diff --git a/feature-libs/user/profile/components/forgot-password/forgot-password.component.spec.ts b/feature-libs/user/profile/components/forgot-password/forgot-password.component.spec.ts index e76c50efa84..b58e1c4f30c 100644 --- a/feature-libs/user/profile/components/forgot-password/forgot-password.component.spec.ts +++ b/feature-libs/user/profile/components/forgot-password/forgot-password.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { @@ -18,7 +19,6 @@ import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feat import { BehaviorSubject } from 'rxjs'; import { ForgotPasswordComponentService } from './forgot-password-component.service'; import { ForgotPasswordComponent } from './forgot-password.component'; -import createSpy = jasmine.createSpy; const isBusySubject = new BehaviorSubject(false); class MockForgotPasswordService @@ -28,8 +28,8 @@ class MockForgotPasswordService userEmail: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - requestEmail = createSpy().and.stub(); - resetForm = createSpy().and.stub(); + requestEmail = vi.fn().mockImplementation(() => {}); + resetForm = vi.fn().mockImplementation(() => {}); } class MockRoutingService implements Partial { @@ -43,7 +43,7 @@ describe('ForgotPasswordComponent', () => { let service: ForgotPasswordComponentService; let routingService: RoutingService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ForgotPasswordComponent], providers: [ @@ -78,7 +78,7 @@ describe('ForgotPasswordComponent', () => { add: { imports: [MockTranslatePipe] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ForgotPasswordComponent); @@ -127,7 +127,7 @@ describe('ForgotPasswordComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); @@ -139,7 +139,7 @@ describe('ForgotPasswordComponent', () => { }); it('should navigate to login on cancel', () => { - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); const cancelBtn = el.query(By.css('button.btn-secondary')); cancelBtn.triggerEventHandler('click'); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'login' }); diff --git a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts index 41df992ceca..f393602ce0c 100644 --- a/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts +++ b/feature-libs/user/profile/components/otp-login-register/otp-login-register.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* * SPDX-FileCopyrightText: 2025 SAP Spartacus team * @@ -5,7 +6,7 @@ */ import { HttpErrorResponse } from '@angular/common/http'; import { Component, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; @@ -45,8 +46,6 @@ import { RegisterComponentService } from '../register'; import { ONE_TIME_PASSWORD_REGISTRATION_PURPOSE } from '../user-account-constants'; import { OneTimePasswordRegisterComponent } from './otp-login-register.component'; -import createSpy = jasmine.createSpy; - const mockRegisterFormData: any = { titleCode: 'Mr', firstName: 'John', @@ -80,15 +79,15 @@ class MockUrlPipe implements PipeTransform { class MockSpinnerComponent {} class MockGlobalMessageService { - add = createSpy(); - remove = createSpy(); + add = vi.fn(); + remove = vi.fn(); get() { return EMPTY; } } class MockRoutingService { - go = createSpy(); + go = vi.fn(); } class MockAnonymousConsentsService { @@ -115,9 +114,9 @@ const mockAnonymousConsentsConfig: AnonymousConsentsConfig = { class MockRegisterComponentService implements Partial { - getTitles = createSpy().and.returnValue(of(mockTitlesList)); - getAdditionalConsents = createSpy(); - generateAdditionalConsentsFormControl = createSpy(); + getTitles = vi.fn().mockReturnValue(of(mockTitlesList)); + getAdditionalConsents = vi.fn(); + generateAdditionalConsentsFormControl = vi.fn(); } class MockSiteAdapter { @@ -147,7 +146,7 @@ class MockLanguageService { class MockClientAuthenticationTokenService implements Partial { - loadClientAuthenticationToken = createSpy().and.returnValue(of(undefined)); + loadClientAuthenticationToken = vi.fn().mockReturnValue(of(undefined)); } describe('OneTimePasswordRegisterComponent', () => { @@ -160,7 +159,7 @@ describe('OneTimePasswordRegisterComponent', () => { let anonymousConsentService: AnonymousConsentsService; let registrationVerificationTokenFacade: VerificationTokenFacade; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -222,7 +221,7 @@ describe('OneTimePasswordRegisterComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(OneTimePasswordRegisterComponent); @@ -269,7 +268,7 @@ describe('OneTimePasswordRegisterComponent', () => { }); it('should handle error when title code is required from the backend config', () => { - spyOn(globalMessageService, 'get').and.returnValue( + vi.spyOn(globalMessageService, 'get').mockReturnValue( of({ [GlobalMessageType.MSG_TYPE_ERROR]: [ { raw: 'This field is required.' }, @@ -292,10 +291,10 @@ describe('OneTimePasswordRegisterComponent', () => { describe('sendRegistrationVerificationToken', () => { it('should create registration verification token with valid form', () => { - spyOn( + vi.spyOn( registrationVerificationTokenFacade, 'createVerificationToken' - ).and.returnValue( + ).mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -313,10 +312,10 @@ describe('OneTimePasswordRegisterComponent', () => { }); it('should not create registration verification token with invalid form', () => { - spyOn( + vi.spyOn( registrationVerificationTokenFacade, 'createVerificationToken' - ).and.returnValue( + ).mockReturnValue( of({ expiresIn: '300', tokenId: 'mockTokenId', @@ -342,10 +341,10 @@ describe('OneTimePasswordRegisterComponent', () => { url: 'https://localhost:9002/occ/v2/electronics-spa/users/anonymous/verificationToken?lang=en&curr=USD', }); component.ngOnInit(); - spyOn( + vi.spyOn( registrationVerificationTokenFacade, 'createVerificationToken' - ).and.returnValue(throwError(() => httpErrorResponse)); + ).mockReturnValue(throwError(() => httpErrorResponse)); component.sendRegistrationVerificationToken(); expect(mockRoutingService.go).toHaveBeenCalled(); @@ -355,7 +354,9 @@ describe('OneTimePasswordRegisterComponent', () => { const toggleAnonymousConsentMethod = 'toggleAnonymousConsent'; describe(`${toggleAnonymousConsentMethod}`, () => { it('should call anonymousConsentsService.giveConsent when the consent is given', () => { - spyOn(anonymousConsentService, 'giveConsent').and.stub(); + vi.spyOn(anonymousConsentService, 'giveConsent').mockImplementation( + () => {} + ); component.ngOnInit(); controls['newsletter'].setValue(true); @@ -363,7 +364,9 @@ describe('OneTimePasswordRegisterComponent', () => { expect(anonymousConsentService.giveConsent).toHaveBeenCalled(); }); it('should call anonymousConsentsService.withdrawConsent when the consent is NOT given', () => { - spyOn(anonymousConsentService, 'withdrawConsent').and.stub(); + vi.spyOn(anonymousConsentService, 'withdrawConsent').mockImplementation( + () => {} + ); component.ngOnInit(); controls['newsletter'].setValue(false); @@ -374,7 +377,9 @@ describe('OneTimePasswordRegisterComponent', () => { describe('isConsentGiven', () => { it('should call anonymousConsentsService.isConsentGiven', () => { - spyOn(anonymousConsentService, 'isConsentGiven').and.stub(); + vi.spyOn(anonymousConsentService, 'isConsentGiven').mockImplementation( + () => {} + ); const mockConsent: AnonymousConsent = { consentState: ANONYMOUS_CONSENT_STATUS.GIVEN, }; @@ -392,7 +397,7 @@ describe('OneTimePasswordRegisterComponent', () => { }); it('should disable input when register consent is required', () => { - spyOn(component, isConsentRequiredMethod).and.returnValue(true); + vi.spyOn(component, isConsentRequiredMethod).mockReturnValue(true); fixture.detectChanges(); expect(controls['newsletter'].status).toEqual('DISABLED'); }); @@ -402,7 +407,7 @@ describe('OneTimePasswordRegisterComponent', () => { let captchaComponent; beforeEach(() => { captchaComponent = fixture.debugElement.query(By.css('cx-captcha')); - spyOn(component, 'sendRegistrationVerificationToken').and.callThrough(); + vi.spyOn(component, 'sendRegistrationVerificationToken'); mockRegisterFormData.captcha = false; component.registerForm.patchValue(mockRegisterFormData); }); @@ -428,7 +433,7 @@ describe('OneTimePasswordRegisterComponent', () => { }); it('should confirm captcha', () => { - spyOn(component, 'captchaConfirmed').and.callThrough(); + vi.spyOn(component, 'captchaConfirmed'); captchaComponent.triggerEventHandler('enabled', true); captchaComponent.triggerEventHandler('confirmed', true); diff --git a/feature-libs/user/profile/components/register/register-component.service.spec.ts b/feature-libs/user/profile/components/register/register-component.service.spec.ts index 7df61d48bbc..e354497ba8c 100644 --- a/feature-libs/user/profile/components/register/register-component.service.spec.ts +++ b/feature-libs/user/profile/components/register/register-component.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { UntypedFormBuilder } from '@angular/forms'; import { GlobalMessageService, GlobalMessageType } from '@spartacus/core'; @@ -5,7 +6,6 @@ import { UserRegisterFacade, UserSignUp } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { RegisterComponentService } from './register-component.service'; -import createSpy = jasmine.createSpy; const mockRegisterFormData: any = { titleCode: 'Mr', firstName: 'John', @@ -20,11 +20,11 @@ const mockRegisterFormData: any = { }; class MockUserRegisterFacade implements Partial { - getTitles = createSpy().and.returnValue(of([])); - register = createSpy().and.callFake((user: any) => of(user)); + getTitles = vi.fn().mockReturnValue(of([])); + register = vi.fn().mockImplementation((user: any) => of(user)); } class MockGlobalMessageService implements Partial { - add = createSpy(); + add = vi.fn(); } describe('RegisterComponentService', () => { @@ -90,7 +90,7 @@ describe('RegisterComponentService', () => { }); it('generateAdditionalConsentsFormControl', () => { - spyOn(fb, 'array').and.callThrough(); + vi.spyOn(fb, 'array'); service.generateAdditionalConsentsFormControl(); expect(fb.array).toHaveBeenCalled(); }); diff --git a/feature-libs/user/profile/components/register/register.component.spec.ts b/feature-libs/user/profile/components/register/register.component.spec.ts index 0b61b760153..bca8255253e 100644 --- a/feature-libs/user/profile/components/register/register.component.spec.ts +++ b/feature-libs/user/profile/components/register/register.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { Component, DebugElement, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AbstractControl, ReactiveFormsModule, @@ -45,7 +46,6 @@ import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feat import { EMPTY, Observable, Subject, of } from 'rxjs'; import { RegisterComponentService } from './register-component.service'; import { RegisterComponent } from './register.component'; -import createSpy = jasmine.createSpy; const mockSecurePassword = 'strongPas$!123'; const mockInvalidPassword = 'strongPas$!123|'; @@ -95,15 +95,15 @@ class MockUrlPipe implements PipeTransform { class MockSpinnerComponent {} class MockGlobalMessageService { - add = createSpy(); - remove = createSpy(); + add = vi.fn(); + remove = vi.fn(); get() { return EMPTY; } } class MockRoutingService { - go = createSpy(); + go = vi.fn(); } class MockAnonymousConsentsService { @@ -136,12 +136,12 @@ const mockAnonymousConsentsConfig: AnonymousConsentsConfig = { class MockRegisterComponentService implements Partial { - getTitles = createSpy().and.returnValue(of(mockTitlesList)); - register = createSpy().and.returnValue(of(undefined)); - postRegisterMessage = createSpy(); - getAdditionalConsents = createSpy(); - generateAdditionalConsentsFormControl = createSpy(); - collectDataFromRegisterForm = createSpy(); + getTitles = vi.fn().mockReturnValue(of(mockTitlesList)); + register = vi.fn().mockReturnValue(of(undefined)); + postRegisterMessage = vi.fn(); + getAdditionalConsents = vi.fn(); + generateAdditionalConsentsFormControl = vi.fn(); + collectDataFromRegisterForm = vi.fn(); } class MockSiteAdapter { @@ -181,7 +181,7 @@ describe('RegisterComponent', () => { let registerComponentService: RegisterComponentService; let featureToggles: FeatureToggles; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -253,7 +253,7 @@ describe('RegisterComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(RegisterComponent); @@ -301,7 +301,7 @@ describe('RegisterComponent', () => { }); it('should handle error when title code is required from the backend config', () => { - spyOn(globalMessageService, 'get').and.returnValue( + vi.spyOn(globalMessageService, 'get').mockReturnValue( of({ [GlobalMessageType.MSG_TYPE_ERROR]: [ { raw: 'This field is required.' }, @@ -323,7 +323,7 @@ describe('RegisterComponent', () => { it('should show spinner when loading = true', () => { const register = new Subject(); - (regComponentService.register as any).and.returnValue(register); + (regComponentService.register as any).mockReturnValue(register); component.ngOnInit(); component.registerUser(); fixture.detectChanges(); @@ -353,8 +353,9 @@ describe('RegisterComponent', () => { describe('register', () => { it('should register with valid form', () => { - regComponentService.collectDataFromRegisterForm = - createSpy().and.returnValue({ + regComponentService.collectDataFromRegisterForm = vi + .fn() + .mockReturnValue({ firstName: mockRegisterFormData.firstName, lastName: mockRegisterFormData.lastName, uid: mockRegisterFormData.email_lowercase, @@ -389,7 +390,7 @@ describe('RegisterComponent', () => { }); it('should not redirect in different flow that ResourceOwnerPasswordFlow', () => { - spyOn(authConfigService, 'getOAuthFlow').and.returnValue( + vi.spyOn(authConfigService, 'getOAuthFlow').mockReturnValue( OAuthFlow.ImplicitFlow ); component.ngOnInit(); @@ -403,7 +404,9 @@ describe('RegisterComponent', () => { const toggleAnonymousConsentMethod = 'toggleAnonymousConsent'; describe(`${toggleAnonymousConsentMethod}`, () => { it('should call anonymousConsentsService.giveConsent when the consent is given', () => { - spyOn(anonymousConsentService, 'giveConsent').and.stub(); + vi.spyOn(anonymousConsentService, 'giveConsent').mockImplementation( + () => {} + ); component.ngOnInit(); controls['newsletter'].setValue(true); @@ -411,7 +414,9 @@ describe('RegisterComponent', () => { expect(anonymousConsentService.giveConsent).toHaveBeenCalled(); }); it('should call anonymousConsentsService.withdrawConsent when the consent is NOT given', () => { - spyOn(anonymousConsentService, 'withdrawConsent').and.stub(); + vi.spyOn(anonymousConsentService, 'withdrawConsent').mockImplementation( + () => {} + ); component.ngOnInit(); controls['newsletter'].setValue(false); @@ -422,7 +427,9 @@ describe('RegisterComponent', () => { describe('isConsentGiven', () => { it('should call anonymousConsentsService.isConsentGiven', () => { - spyOn(anonymousConsentService, 'isConsentGiven').and.stub(); + vi.spyOn(anonymousConsentService, 'isConsentGiven').mockImplementation( + () => {} + ); const mockConsent: AnonymousConsent = { consentState: ANONYMOUS_CONSENT_STATUS.GIVEN, }; @@ -440,7 +447,8 @@ describe('RegisterComponent', () => { }); it('should disable input when register consent is required', () => { - spyOn(component, isConsentRequiredMethod).and.returnValue(true); + vi.spyOn(component, isConsentRequiredMethod).mockReturnValue(true); + fixture.detectChanges(); fixture.detectChanges(); expect(controls['newsletter'].status).toEqual('DISABLED'); }); @@ -450,7 +458,7 @@ describe('RegisterComponent', () => { let captchaComponent: DebugElement; beforeEach(() => { captchaComponent = fixture.debugElement.query(By.css('cx-captcha')); - spyOn(component, 'registerUser').and.callThrough(); + vi.spyOn(component, 'registerUser'); mockRegisterFormData.captcha = false; component.registerForm.patchValue(mockRegisterFormData); }); @@ -472,7 +480,7 @@ describe('RegisterComponent', () => { }); it('should confirm captcha', () => { - spyOn(component, 'captchaConfirmed').and.callThrough(); + vi.spyOn(component, 'captchaConfirmed'); captchaComponent.triggerEventHandler('enabled', true); captchaComponent.triggerEventHandler('confirmed', true); diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts index 4351a08a180..6d83a3df53f 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.component.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* * SPDX-FileCopyrightText: 2025 SAP Spartacus team * @@ -5,7 +6,7 @@ */ import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectorRef, DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -34,7 +35,6 @@ import { MockUrlPipe } from 'core-libs/core/src/routing/configurable-routes/url- import { BehaviorSubject, EMPTY, of, throwError } from 'rxjs'; import { RegistrationVerificationTokenFormComponent } from './verify-register-verification-token-form.component'; import { RegistrationVerificationTokenFormComponentService } from './verify-register-verification-token-form.service'; -import createSpy = jasmine.createSpy; const mockSecurePassword = 'strongPas$!123'; const mockInvalidPassword = 'strongPas$!123|'; @@ -51,7 +51,7 @@ const mockRegisterFormData: any = { }; class MockRoutingService { - go = createSpy(); + go = vi.fn(); } class MockFormComponentService @@ -62,26 +62,26 @@ class MockFormComponentService tokenCode: new UntypedFormControl(), }); isUpdating$ = new BehaviorSubject(false); - createVerificationToken = createSpy().and.returnValue( - of({ tokenId: 'testTokenId', expiresIn: '300' }) - ); - displayMessage = createSpy('displayMessage').and.stub(); + createVerificationToken = vi + .fn() + .mockReturnValue(of({ tokenId: 'testTokenId', expiresIn: '300' })); + displayMessage = vi.fn('displayMessage').mockImplementation(() => {}); } class MockLaunchDialogService implements Partial { - openDialogAndSubscribe = createSpy().and.stub(); + openDialogAndSubscribe = vi.fn().mockImplementation(() => {}); } class MockRegistrationVerificationTokenFormComponentService implements Partial { - postRegisterMessage = createSpy(); - displayMessage = createSpy(); + postRegisterMessage = vi.fn(); + displayMessage = vi.fn(); } class MockGlobalMessageService { - add = createSpy(); - remove = createSpy(); + add = vi.fn(); + remove = vi.fn(); get() { return EMPTY; } @@ -97,7 +97,7 @@ describe('RegistrationVerificationTokenFormComponent', () => { let routingService: RoutingService; let featureToggles: FeatureToggles; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -136,7 +136,7 @@ describe('RegistrationVerificationTokenFormComponent', () => { add: { imports: [MockTranslatePipe, MockUrlPipe] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent( @@ -171,14 +171,14 @@ describe('RegistrationVerificationTokenFormComponent', () => { describe('register', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); }); it('should display send verification token sucessful message when history state is valid', () => { - spyOn(component, 'startWaitTimeInterval'); + vi.spyOn(component, 'startWaitTimeInterval'); component.ngOnInit(); expect(component.startWaitTimeInterval).toHaveBeenCalled(); expect(service.displayMessage).toHaveBeenCalledWith( @@ -188,7 +188,7 @@ describe('RegistrationVerificationTokenFormComponent', () => { }); it('should register with valid form', () => { - service.register = createSpy().and.returnValue(of(mockRegisterFormData)); + service.register = vi.fn().mockReturnValue(of(mockRegisterFormData)); component.registerForm.patchValue(mockRegisterFormData); component.ngOnInit(); component.onSubmit(); @@ -204,15 +204,15 @@ describe('RegistrationVerificationTokenFormComponent', () => { }); it('should not register with invalid form', () => { - service.register = createSpy(); + service.register = vi.fn(); component.ngOnInit(); component.onSubmit(); expect(service.register).not.toHaveBeenCalled(); }); it('should not redirect in different flow that ResourceOwnerPasswordFlow', () => { - service.register = createSpy().and.returnValue(of(mockRegisterFormData)); - spyOn(authConfigService, 'getOAuthFlow').and.returnValue( + service.register = vi.fn().mockReturnValue(of(mockRegisterFormData)); + vi.spyOn(authConfigService, 'getOAuthFlow').mockReturnValue( OAuthFlow.ImplicitFlow ); component.ngOnInit(); @@ -222,8 +222,8 @@ describe('RegistrationVerificationTokenFormComponent', () => { }); it('should redirect in different flow that ResourceOwnerPasswordFlow', () => { - service.register = createSpy().and.returnValue(of(mockRegisterFormData)); - spyOn(authConfigService, 'getOAuthFlow').and.returnValue( + service.register = vi.fn().mockReturnValue(of(mockRegisterFormData)); + vi.spyOn(authConfigService, 'getOAuthFlow').mockReturnValue( OAuthFlow.ResourceOwnerPasswordFlow ); component.ngOnInit(); @@ -235,9 +235,9 @@ describe('RegistrationVerificationTokenFormComponent', () => { const httpErrorResponse = new HttpErrorResponse({ status: 400, }); - service.register = createSpy().and.returnValue( - throwError(() => httpErrorResponse) - ); + service.register = vi + .fn() + .mockReturnValue(throwError(() => httpErrorResponse)); component.registerForm.patchValue(mockRegisterFormData); component.ngOnInit(); component.onSubmit(); @@ -269,10 +269,11 @@ describe('RegistrationVerificationTokenFormComponent', () => { it('should resend OTP', () => { component.target = 'example@example.com'; - spyOn(component, 'startWaitTimeInterval'); - spyOn(component, 'createRegistrationVerificationToken').and.returnValue( - of({ tokenId: 'mock_tokenId', expiresIn: '300' }) - ); + vi.spyOn(component, 'startWaitTimeInterval'); + vi.spyOn( + component, + 'createRegistrationVerificationToken' + ).mockReturnValue(of({ tokenId: 'mock_tokenId', expiresIn: '300' })); component.resendOTP(); diff --git a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts index 3ba4ee9ef0b..8dde03d8508 100644 --- a/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts +++ b/feature-libs/user/profile/components/registration-verification-token-form/verify-register-verification-token-form.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* * SPDX-FileCopyrightText: 2025 SAP Spartacus team * @@ -9,12 +10,11 @@ import { GlobalMessageService, GlobalMessageType } from '@spartacus/core'; import { UserRegisterFacade, UserSignUp } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; -import createSpy = jasmine.createSpy; import { RegistrationVerificationTokenFormComponentService } from './verify-register-verification-token-form.service'; class MockUserRegisterFacade implements Partial { - getTitles = createSpy().and.returnValue(of([])); - register = createSpy().and.callFake((user: any) => of(user)); + getTitles = vi.fn().mockReturnValue(of([])); + register = vi.fn().mockImplementation((user: any) => of(user)); } class MockGlobalMessageService implements Partial { add() {} @@ -50,7 +50,7 @@ describe('RegistrationVerificationTokenFormComponentService', () => { )); it('should display a success message after registration', () => { - spyOn(globalMessageService, 'add'); + vi.spyOn(globalMessageService, 'add'); const userRegisterFormData: UserSignUp = { titleCode: 'Mr.', firstName: 'firstName', @@ -66,7 +66,7 @@ describe('RegistrationVerificationTokenFormComponentService', () => { expect(globalMessageService.add).toHaveBeenCalledWith( { key: 'register.postRegisterSuccessMessage', - params: Object(10000), + params: 10000, }, GlobalMessageType.MSG_TYPE_CONFIRMATION, 10000 @@ -93,7 +93,7 @@ describe('RegistrationVerificationTokenFormComponentService', () => { describe('postRegisterMessage', () => { it('should delegate to displayMessage', () => { - const displayMessageSpy = spyOn(service, 'displayMessage'); + const displayMessageSpy = vi.spyOn(service, 'displayMessage'); service.postRegisterMessage(); expect(displayMessageSpy).toHaveBeenCalled(); }); diff --git a/feature-libs/user/profile/components/reset-password/reset-password-component.service.spec.ts b/feature-libs/user/profile/components/reset-password/reset-password-component.service.spec.ts index a957555333f..1c9feb7236e 100644 --- a/feature-libs/user/profile/components/reset-password/reset-password-component.service.spec.ts +++ b/feature-libs/user/profile/components/reset-password/reset-password-component.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; import { @@ -17,7 +18,6 @@ import { import { UserPasswordFacade } from '@spartacus/user/profile/root'; import { BehaviorSubject, of, throwError } from 'rxjs'; import { ResetPasswordComponentService } from './reset-password-component.service'; -import createSpy = jasmine.createSpy; const resetToken = '123#Token'; const routerState$: BehaviorSubject = new BehaviorSubject({ @@ -35,7 +35,7 @@ class MockUserPasswordFacade implements Partial { } class MockRoutingService { - go = createSpy().and.stub(); + go = vi.fn().mockImplementation(() => {}); getRouterState() { return routerState$; @@ -43,7 +43,7 @@ class MockRoutingService { } class MockGlobalMessageService { - add = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); } describe('ResetPasswordComponentService', () => { @@ -152,7 +152,7 @@ describe('ResetPasswordComponentService', () => { }); it('should reset password', () => { - spyOn(userPasswordService, 'reset').and.callThrough(); + vi.spyOn(userPasswordService, 'reset'); service.resetPassword(resetToken); expect(userPasswordService.reset).toHaveBeenCalledWith( resetToken, @@ -174,7 +174,7 @@ describe('ResetPasswordComponentService', () => { }); it('should reset form', () => { - spyOn(service.form, 'reset').and.callThrough(); + vi.spyOn(service.form, 'reset'); service.resetPassword(resetToken); expect(service.form.reset).toHaveBeenCalled(); }); @@ -190,7 +190,7 @@ describe('ResetPasswordComponentService', () => { it('should show error message', () => { const error = new HttpErrorModel(); error.details = [{ message: 'error message' }]; - spyOn(userPasswordService, 'reset').and.returnValue( + vi.spyOn(userPasswordService, 'reset').mockReturnValue( throwError(() => error) ); service.resetPassword(resetToken); @@ -201,7 +201,7 @@ describe('ResetPasswordComponentService', () => { }); it('should not show error message when error is null', () => { - spyOn(userPasswordService, 'reset').and.returnValue( + vi.spyOn(userPasswordService, 'reset').mockReturnValue( throwError(() => null) ); service.resetPassword(resetToken); @@ -209,7 +209,7 @@ describe('ResetPasswordComponentService', () => { }); it('should not display an error message when HttpErrorModel has no details', () => { - spyOn(userPasswordService, 'reset').and.returnValue( + vi.spyOn(userPasswordService, 'reset').mockReturnValue( throwError(() => new HttpErrorModel()) ); service.resetPassword(resetToken); @@ -219,7 +219,7 @@ describe('ResetPasswordComponentService', () => { }); it('should not reset invalid form', () => { - spyOn(userPasswordService, 'reset').and.returnValue( + vi.spyOn(userPasswordService, 'reset').mockReturnValue( throwError(() => ({})) ); passwordConfirm.setValue('Diff123!'); diff --git a/feature-libs/user/profile/components/reset-password/reset-password.component.spec.ts b/feature-libs/user/profile/components/reset-password/reset-password.component.spec.ts index e8122b97b82..daec23a9611 100644 --- a/feature-libs/user/profile/components/reset-password/reset-password.component.spec.ts +++ b/feature-libs/user/profile/components/reset-password/reset-password.component.spec.ts @@ -1,5 +1,6 @@ +import { vi } from 'vitest'; import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -17,7 +18,6 @@ import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feat import { BehaviorSubject } from 'rxjs'; import { ResetPasswordComponentService } from './reset-password-component.service'; import { ResetPasswordComponent } from './reset-password.component'; -import createSpy = jasmine.createSpy; const isBusySubject = new BehaviorSubject(false); const tokenSubject: BehaviorSubject = new BehaviorSubject('123'); @@ -31,8 +31,8 @@ class MockResetPasswordService passwordConfirm: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - resetPassword = createSpy().and.stub(); - resetForm = createSpy().and.stub(); + resetPassword = vi.fn().mockImplementation(() => {}); + resetForm = vi.fn().mockImplementation(() => {}); } describe('ResetPasswordComponent', () => { @@ -41,7 +41,7 @@ describe('ResetPasswordComponent', () => { let el: DebugElement; let service: ResetPasswordComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -65,7 +65,7 @@ describe('ResetPasswordComponent', () => { add: { imports: [MockTranslatePipe, MockFeatureDirective] }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(ResetPasswordComponent); @@ -122,7 +122,7 @@ describe('ResetPasswordComponent', () => { }); it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); diff --git a/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts index 645b4635cdf..cb721d60a1e 100644 --- a/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts +++ b/feature-libs/user/profile/components/update-email/my-account-v2-email.component.spec.ts @@ -1,9 +1,25 @@ +import { vi } from 'vitest'; + +vi.mock('@spartacus/storefront', async (importActual) => { + const actual = await importActual(); + const { filter, map } = await import('rxjs/operators'); + const isNotNullable = (value: T): value is NonNullable => value != null; + return { + ...actual, + getPageTitle: (pageMetaService: any) => + pageMetaService.getMeta().pipe( + filter(isNotNullable), + map((meta: any) => (meta.heading || meta.title) ?? '') + ), + }; +}); + import { ChangeDetectionStrategy, Component, DebugElement, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -12,6 +28,7 @@ import { import { By } from '@angular/platform-browser'; import { CxDatePipe, + FeatureDirective, GlobalMessageService, I18nTestingModule, MockDatePipe, @@ -27,6 +44,7 @@ import { PasswordVisibilityToggleModule, SpinnerComponent, } from '@spartacus/storefront'; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { MockFeatureTogglesController, provideMockFeatureToggles, @@ -37,7 +55,6 @@ import { BehaviorSubject, Subject, of } from 'rxjs'; import { UserProfileFacade } from '../../root/facade'; import { MyAccountV2EmailComponent } from './my-account-v2-email.component'; import { UpdateEmailComponentService } from './update-email-component.service'; -import createSpy = jasmine.createSpy; const mockPageMeta: PageMeta = { title: 'Test Title', heading: 'Test Heading' }; class MockPageMetaService implements Partial { @@ -69,12 +86,12 @@ class MockMyAccountV2EmailService password: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - save = createSpy().and.stub(); - resetForm = createSpy().and.stub(); + save = vi.fn().mockImplementation(() => {}); + resetForm = vi.fn().mockImplementation(() => {}); } class MockGlobalMessageService implements Partial { - add = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); } const sampleUser: User = { @@ -93,7 +110,7 @@ describe('MyAccountV2EmailComponent', () => { let service: UpdateEmailComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -117,7 +134,13 @@ describe('MyAccountV2EmailComponent', () => { }) .overrideComponent(MyAccountV2EmailComponent, { remove: { - imports: [TranslatePipe, CxDatePipe, UrlPipe, SpinnerComponent], + imports: [ + TranslatePipe, + CxDatePipe, + UrlPipe, + SpinnerComponent, + FeatureDirective, + ], }, add: { imports: [ @@ -125,23 +148,30 @@ describe('MyAccountV2EmailComponent', () => { MockDatePipe, MockUrlPipe, MockCxSpinnerComponent, + MockFeatureDirective, ], changeDetection: ChangeDetectionStrategy.Default, }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(MyAccountV2EmailComponent); component = fixture.componentInstance; - component.onEdit(); el = fixture.debugElement; service = TestBed.inject(UpdateEmailComponentService); TestBed.inject(UserProfileFacade); - fixture.detectChanges(); + fixture.detectChanges(); // trigger ngOnInit first + component.onEdit(); }); + // Helper to run CD without triggering checkNoChanges (avoids NG0100 from + // async pipes re-subscribing to synchronously-emitting observables) + function detectChanges() { + fixture.componentRef.changeDetectorRef.detectChanges(); + } + it('should create', () => { expect(component).toBeTruthy(); }); @@ -150,7 +180,7 @@ describe('MyAccountV2EmailComponent', () => { it('should disable the submit button when form is disabled', () => { component.form.disable(); component.onEdit(); - fixture.detectChanges(); + detectChanges(); const submitBtn: HTMLButtonElement = el.query( By.css('.btn-primary') ).nativeElement; @@ -159,7 +189,7 @@ describe('MyAccountV2EmailComponent', () => { it('should show the spinner', () => { isBusySubject.next(true); - fixture.detectChanges(); + detectChanges(); expect(el.query(By.css('cx-spinner'))).toBeTruthy(); }); }); @@ -168,20 +198,20 @@ describe('MyAccountV2EmailComponent', () => { it('should enable the submit button', () => { component.form.enable(); component.onEdit(); - fixture.detectChanges(); + detectChanges(); const submitBtn = el.query(By.css('.btn-primary')); expect(submitBtn.nativeElement.disabled).toBeFalsy(); }); it('should not show the spinner', () => { isBusySubject.next(false); - fixture.detectChanges(); + detectChanges(); expect(el.query(By.css('cx-spinner'))).toBeNull(); }); it('should show cx message strip', () => { component.onEdit(); - fixture.detectChanges(); + detectChanges(); const cxMsg = el.query(By.css('cx-message')); expect(cxMsg.nativeElement).toBeTruthy(); }); @@ -189,7 +219,7 @@ describe('MyAccountV2EmailComponent', () => { it('should hide cx message strip when close clicked', () => { component.onEdit(); component.closeDialogConfirmationAlert(); - fixture.detectChanges(); + detectChanges(); const cxMsg = el.query(By.css('cx-message')); expect(cxMsg).toBeNull(); }); @@ -198,7 +228,7 @@ describe('MyAccountV2EmailComponent', () => { describe('idle - display', () => { it('should hide the submit button', () => { component.ngOnInit(); - fixture.detectChanges(); + detectChanges(); expect(el.query(By.css('form'))).toBeNull(); }); }); @@ -206,8 +236,8 @@ describe('MyAccountV2EmailComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { component.onEdit(); - fixture.detectChanges(); - const request = spyOn(component, 'onSubmit'); + detectChanges(); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); @@ -223,7 +253,7 @@ describe('MyAccountV2EmailComponent', () => { it('when cancel is called. submit button is not visible', () => { component.form.enable(); component.cancelEdit(); - fixture.detectChanges(); + detectChanges(); const submitBtn = el.query(By.css('button.btn-primary')); expect(submitBtn).toBeNull(); }); @@ -240,7 +270,7 @@ describe('MyAccountV2EmailComponent', () => { beforeEach(() => { toggleController.set('a11yFormFieldSectionLegend', true); component.onEdit(); - fixture.detectChanges(); + detectChanges(); }); it('should render a fieldset with a visible legend', () => { @@ -256,7 +286,7 @@ describe('MyAccountV2EmailComponent', () => { beforeEach(() => { toggleController.set('a11yFormFieldSectionLegend', false); component.onEdit(); - fixture.detectChanges(); + detectChanges(); }); it('should render a fieldset', () => { diff --git a/feature-libs/user/profile/components/update-email/update-email-component.service.spec.ts b/feature-libs/user/profile/components/update-email/update-email-component.service.spec.ts index 738c803e384..75f2fce8db2 100644 --- a/feature-libs/user/profile/components/update-email/update-email-component.service.spec.ts +++ b/feature-libs/user/profile/components/update-email/update-email-component.service.spec.ts @@ -1,4 +1,5 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { TestBed } from '@angular/core/testing'; import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; import { AuthRedirectService, @@ -12,23 +13,22 @@ import { FormErrorsModule } from '@spartacus/storefront'; import { UserEmailFacade } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { UpdateEmailComponentService } from './update-email-component.service'; -import createSpy = jasmine.createSpy; class MockUserEmailService implements Partial { - update = createSpy().and.returnValue(of({})); + update = vi.fn().mockReturnValue(of({})); } class MockAuthService { - coreLogout = createSpy().and.returnValue(Promise.resolve()); + coreLogout = vi.fn().mockReturnValue(Promise.resolve()); } class MockRoutingService { - go = createSpy().and.stub(); - getUrl = createSpy().and.returnValue(''); + go = vi.fn().mockImplementation(() => {}); + getUrl = vi.fn().mockReturnValue(''); } class MockGlobalMessageService { - add = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); } class MockAuthRedirectService implements Partial { - setRedirectUrl = createSpy('setRedirectUrl'); + setRedirectUrl = vi.fn(); } describe('UpdateEmailComponentService', () => { @@ -94,16 +94,16 @@ describe('UpdateEmailComponentService', () => { service['busy$'].next(true); let result; service.isUpdating$.subscribe((value) => (result = value)).unsubscribe(); - expect(result).toBeTrue(); - expect(service.form.disabled).toBeTrue(); + expect(result).toBe(true); + expect(service.form.disabled).toBe(true); }); it('should return false', () => { service['busy$'].next(false); let result; service.isUpdating$.subscribe((value) => (result = value)).unsubscribe(); - expect(result).toBeFalse; - expect(service.form.disabled).toBeFalse(); + expect(result).toBe(false); + expect(service.form.disabled).toBe(false); }); }); @@ -139,7 +139,7 @@ describe('UpdateEmailComponentService', () => { expect(authService.coreLogout).toHaveBeenCalled(); }); - it('should reroute to the login page', waitForAsync(() => { + it('should reroute to the login page', async () => { service.save(); authService.coreLogout().then(() => { expect(routingService.go).toHaveBeenCalledWith( @@ -151,15 +151,15 @@ describe('UpdateEmailComponentService', () => { } ); }); - })); + }); it('reset form', () => { - spyOn(service.form, 'reset').and.callThrough(); + vi.spyOn(service.form, 'reset'); service.save(); expect(service.form.reset).toHaveBeenCalled(); }); - it('should set the redirect url to the home page before navigating to the login page', waitForAsync(() => { + it('should set the redirect url to the home page before navigating to the login page', async () => { service.save(); expect(authRedirectService.setRedirectUrl).toHaveBeenCalledWith( routingService.getUrl({ cxRoute: 'home' }) @@ -169,7 +169,7 @@ describe('UpdateEmailComponentService', () => { routingService.go ); }); - })); + }); }); describe('error', () => { diff --git a/feature-libs/user/profile/components/update-email/update-email.component.spec.ts b/feature-libs/user/profile/components/update-email/update-email.component.spec.ts index da5fc78ce8e..60a37e8568f 100644 --- a/feature-libs/user/profile/components/update-email/update-email.component.spec.ts +++ b/feature-libs/user/profile/components/update-email/update-email.component.spec.ts @@ -1,9 +1,25 @@ +import { vi } from 'vitest'; + +vi.mock('@spartacus/storefront', async (importActual) => { + const actual = await importActual(); + const { filter, map } = await import('rxjs/operators'); + const isNotNullable = (value: T): value is NonNullable => value != null; + return { + ...actual, + getPageTitle: (pageMetaService: any) => + pageMetaService.getMeta().pipe( + filter(isNotNullable), + map((meta: any) => (meta.heading || meta.title) ?? '') + ), + }; +}); + import { ChangeDetectionStrategy, Component, DebugElement, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -13,6 +29,7 @@ import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { CxDatePipe, + FeaturesConfig, I18nTestingModule, MockDatePipe, MockTranslatePipe, @@ -35,7 +52,6 @@ import { UrlTestingModule } from 'core-libs/core/src/routing/configurable-routes import { BehaviorSubject, of } from 'rxjs'; import { UpdateEmailComponentService } from './update-email-component.service'; import { UpdateEmailComponent } from './update-email.component'; -import createSpy = jasmine.createSpy; @Component({ selector: 'cx-spinner', @@ -58,8 +74,8 @@ class MockUpdateEmailService implements Partial { password: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - save = createSpy().and.stub(); - resetForm = createSpy().and.stub(); + save = vi.fn().mockImplementation(() => {}); + resetForm = vi.fn().mockImplementation(() => {}); } const mockPageMeta: PageMeta = { @@ -77,7 +93,7 @@ describe('UpdateEmailComponent', () => { let service: UpdateEmailComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -92,6 +108,10 @@ describe('UpdateEmailComponent', () => { useClass: MockUpdateEmailService, }, { provide: PageMetaService, useClass: MockPageMetaService }, + { + provide: FeaturesConfig, + useValue: { features: { a11yFormFieldSectionLegend: true } }, + }, ...provideMockFeatureToggles({ a11yFormFieldSectionLegend: true }), ], }) @@ -110,7 +130,7 @@ describe('UpdateEmailComponent', () => { }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(UpdateEmailComponent); @@ -159,7 +179,7 @@ describe('UpdateEmailComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); diff --git a/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts index 87ef1167fed..aca36d642bb 100644 --- a/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts +++ b/feature-libs/user/profile/components/update-password/my-account-v2-password.component.spec.ts @@ -1,9 +1,25 @@ +import { vi } from 'vitest'; + +vi.mock('@spartacus/storefront', async (importActual) => { + const actual = await importActual(); + const { filter, map } = await import('rxjs/operators'); + const isNotNullable = (value: T): value is NonNullable => value != null; + return { + ...actual, + getPageTitle: (pageMetaService: any) => + pageMetaService.getMeta().pipe( + filter(isNotNullable), + map((meta: any) => (meta.heading || meta.title) ?? '') + ), + }; +}); + import { ChangeDetectionStrategy, Component, DebugElement, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -20,12 +36,14 @@ import { PageMetaService, TranslatePipe, UrlPipe, + FeatureDirective, } from '@spartacus/core'; import { FormErrorsModule, PasswordVisibilityToggleModule, SpinnerComponent, } from '@spartacus/storefront'; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { MockFeatureTogglesController, provideMockFeatureToggles, @@ -35,7 +53,6 @@ import { UrlTestingModule } from 'core-libs/core/src/routing/configurable-routes import { BehaviorSubject, of } from 'rxjs'; import { MyAccountV2PasswordComponent } from './my-account-v2-password.component'; import { UpdatePasswordComponentService } from './update-password-component.service'; -import createSpy = jasmine.createSpy; const mockPageMeta: PageMeta = { title: 'Test Title', heading: 'Test Heading' }; class MockPageMetaService implements Partial { @@ -65,12 +82,12 @@ class MockUpdatePasswordService newPasswordConfirm: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - updatePassword = createSpy().and.stub(); - resetForm = createSpy().and.stub(); + updatePassword = vi.fn().mockImplementation(() => {}); + resetForm = vi.fn().mockImplementation(() => {}); } class MockGlobalMessageService implements Partial { - add = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); } describe('MyAccountV2PasswordComponent', () => { @@ -80,7 +97,7 @@ describe('MyAccountV2PasswordComponent', () => { let service: UpdatePasswordComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -100,7 +117,13 @@ describe('MyAccountV2PasswordComponent', () => { }) .overrideComponent(MyAccountV2PasswordComponent, { remove: { - imports: [TranslatePipe, CxDatePipe, UrlPipe, SpinnerComponent], + imports: [ + TranslatePipe, + CxDatePipe, + UrlPipe, + SpinnerComponent, + FeatureDirective, + ], }, add: { imports: [ @@ -108,19 +131,19 @@ describe('MyAccountV2PasswordComponent', () => { MockDatePipe, MockUrlPipe, MockCxSpinnerComponent, + MockFeatureDirective, ], changeDetection: ChangeDetectionStrategy.Default, }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(MyAccountV2PasswordComponent); component = fixture.componentInstance; el = fixture.debugElement; service = TestBed.inject(UpdatePasswordComponentService); - fixture.detectChanges(); }); it('should create', () => { @@ -161,7 +184,8 @@ describe('MyAccountV2PasswordComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + fixture.detectChanges(); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); diff --git a/feature-libs/user/profile/components/update-password/update-password-component.service.spec.ts b/feature-libs/user/profile/components/update-password/update-password-component.service.spec.ts index 88eff9e361e..9401f30cec1 100644 --- a/feature-libs/user/profile/components/update-password/update-password-component.service.spec.ts +++ b/feature-libs/user/profile/components/update-password/update-password-component.service.spec.ts @@ -1,4 +1,5 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { vi } from 'vitest'; +import { TestBed } from '@angular/core/testing'; import { AbstractControl, ReactiveFormsModule, @@ -18,30 +19,29 @@ import { FormErrorsModule } from '@spartacus/storefront'; import { UserPasswordFacade } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { UpdatePasswordComponentService } from './update-password-component.service'; -import createSpy = jasmine.createSpy; const mockSecurePassword = 'strongPas$!123'; const mockInvalidPassword = 'strongPas$!123|'; class MockUserPasswordFacade implements Partial { - update = createSpy().and.returnValue(of({})); + update = vi.fn().mockReturnValue(of({})); } class MockRoutingService implements Partial { - go = createSpy(); - getUrl = createSpy().and.returnValue(''); + go = vi.fn(); + getUrl = vi.fn().mockReturnValue(''); } class MockGlobalMessageService implements Partial { - add = createSpy(); + add = vi.fn(); } class MockAuthRedirectService implements Partial { - setRedirectUrl = createSpy(); + setRedirectUrl = vi.fn(); } class MockAuthService implements Partial { - coreLogout = createSpy().and.returnValue(Promise.resolve()); + coreLogout = vi.fn().mockReturnValue(Promise.resolve()); } describe('UpdatePasswordComponentService', () => { @@ -150,15 +150,17 @@ describe('UpdatePasswordComponentService', () => { ); }); - it('should reroute to the login page', fakeAsync(() => { + it('should reroute to the login page', async () => { + vi.useFakeTimers(); service.updatePassword(); - tick(); + await vi.advanceTimersByTimeAsync(0); + vi.useRealTimers(); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'login' }); - })); + }); it('should reset the form', () => { - spyOn(service.form, 'reset').and.callThrough(); + vi.spyOn(service.form, 'reset'); service.updatePassword(); expect(service.form.reset).toHaveBeenCalled(); }); diff --git a/feature-libs/user/profile/components/update-password/update-password.component.spec.ts b/feature-libs/user/profile/components/update-password/update-password.component.spec.ts index 1d0c9293a70..35b210cd2d1 100644 --- a/feature-libs/user/profile/components/update-password/update-password.component.spec.ts +++ b/feature-libs/user/profile/components/update-password/update-password.component.spec.ts @@ -1,9 +1,10 @@ +import { vi } from 'vitest'; import { ChangeDetectionStrategy, Component, DebugElement, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -11,14 +12,18 @@ import { } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { + FeatureDirective, I18nTestingModule, + MockTranslatePipe, PageMeta, PageMetaService, RoutingService, + TranslatePipe, } from '@spartacus/core'; import { FormErrorsModule, PasswordVisibilityToggleModule, + SpinnerComponent, } from '@spartacus/storefront'; import { UrlTestingModule } from 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module'; import { @@ -28,7 +33,7 @@ import { import { BehaviorSubject, of } from 'rxjs'; import { UpdatePasswordComponentService } from './update-password-component.service'; import { UpdatePasswordComponent } from './update-password.component'; -import createSpy = jasmine.createSpy; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; @Component({ selector: 'cx-spinner', @@ -53,8 +58,8 @@ class MockUpdatePasswordService newPasswordConfirm: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - updatePassword = createSpy().and.stub(); - resetForm = createSpy().and.stub(); + updatePassword = vi.fn().mockImplementation(() => {}); + resetForm = vi.fn().mockImplementation(() => {}); } class MockRoutingService implements Partial { @@ -76,7 +81,7 @@ describe('UpdatePasswordComponent', () => { let routingService: RoutingService; let service: UpdatePasswordComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, @@ -98,10 +103,20 @@ describe('UpdatePasswordComponent', () => { ], }) .overrideComponent(UpdatePasswordComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, + remove: { + imports: [TranslatePipe, SpinnerComponent, FeatureDirective], + }, + add: { + imports: [ + MockTranslatePipe, + MockCxSpinnerComponent, + MockFeatureDirective, + ], + changeDetection: ChangeDetectionStrategy.Default, + }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(UpdatePasswordComponent); @@ -151,7 +166,7 @@ describe('UpdatePasswordComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); @@ -163,7 +178,7 @@ describe('UpdatePasswordComponent', () => { }); it('should navigate to home on cancel', () => { - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); const cancelBtn = el.query(By.css('button.btn-secondary')); cancelBtn.triggerEventHandler('click'); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'home' }); diff --git a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts index a5921701b8f..6d7e5ec8cba 100644 --- a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts +++ b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.spec.ts @@ -1,10 +1,11 @@ -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, DebugElement, + Directive, + Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -13,13 +14,19 @@ import { import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; import { - FeaturesConfigModule, - I18nTestingModule, + FeatureDirective, + FeaturesConfig, + MockTranslatePipe, PageMeta, PageMetaService, + TranslatePipe, } from '@spartacus/core'; -import { FormErrorsModule } from '@spartacus/storefront'; -import { UrlTestingModule } from 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module'; +import { + FormErrorsModule, + NgSelectA11yDirective, + SpinnerComponent, +} from '@spartacus/storefront'; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { MockFeatureTogglesController, provideMockFeatureToggles, @@ -27,27 +34,23 @@ import { import { BehaviorSubject, Subject, of } from 'rxjs'; import { MyAccountV2ProfileComponent } from './my-account-v2-profile.component'; import { UpdateProfileComponentService } from './update-profile-component.service'; -import createSpy = jasmine.createSpy; const mockPageMeta: PageMeta = { title: 'Test Title', heading: 'Test Heading' }; class MockPageMetaService implements Partial { getMeta = () => of(mockPageMeta); } + @Component({ selector: 'cx-spinner', - template: `
spinner
`, - imports: [ - CommonModule, - ReactiveFormsModule, - I18nTestingModule, - FormErrorsModule, - UrlTestingModule, - NgSelectModule, - FeaturesConfigModule, - ], + template: '', }) class MockCxSpinnerComponent {} +@Directive({ selector: '[cxNgSelectA11y]' }) +class MockNgSelectA11yDirective { + @Input() cxNgSelectA11y: { ariaLabel?: string; ariaControls?: string }; +} + const isBusySubject = new BehaviorSubject(false); class MockProfileService implements Partial { @@ -61,7 +64,7 @@ class MockProfileService implements Partial { lastName: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - updateProfile = createSpy().and.stub(); + updateProfile = vi.fn().mockImplementation(() => {}); } describe('MyAccountV2ProfileComponent', () => { @@ -71,44 +74,68 @@ describe('MyAccountV2ProfileComponent', () => { let service: UpdateProfileComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ imports: [ - CommonModule, ReactiveFormsModule, - I18nTestingModule, FormErrorsModule, - UrlTestingModule, NgSelectModule, - FeaturesConfigModule, MyAccountV2ProfileComponent, MockCxSpinnerComponent, + MockNgSelectA11yDirective, ], providers: [ { provide: UpdateProfileComponentService, useClass: MockProfileService, }, + { + provide: FeaturesConfig, + useValue: { + features: { level: '5.2' }, + }, + }, { provide: PageMetaService, useClass: MockPageMetaService }, ...provideMockFeatureToggles({ a11yFormFieldSectionLegend: true }), ], }) .overrideComponent(MyAccountV2ProfileComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, + remove: { + imports: [ + TranslatePipe, + SpinnerComponent, + NgSelectA11yDirective, + FeatureDirective, + ], + }, + add: { + imports: [ + MockTranslatePipe, + MockCxSpinnerComponent, + MockNgSelectA11yDirective, + MockFeatureDirective, + ], + changeDetection: ChangeDetectionStrategy.Default, + }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(MyAccountV2ProfileComponent); component = fixture.componentInstance; el = fixture.debugElement; - component.onEdit(); service = TestBed.inject(UpdateProfileComponentService); - - fixture.detectChanges(); + fixture.detectChanges(); // trigger ngOnInit first + component.onEdit(); }); + // Helper to run CD without triggering checkNoChanges (avoids NG0100 from + // async pipes re-subscribing to synchronously-emitting observables) + function detectChanges() { + fixture.componentRef.changeDetectorRef.detectChanges(); + } + it('should create', () => { expect(component).toBeTruthy(); }); @@ -117,7 +144,7 @@ describe('MyAccountV2ProfileComponent', () => { it('should disable the submit button when form is disabled', () => { component.form.disable(); component.onEdit(); - fixture.detectChanges(); + detectChanges(); const submitBtn: HTMLButtonElement = el.query( By.css('.btn-primary') ).nativeElement; @@ -126,7 +153,7 @@ describe('MyAccountV2ProfileComponent', () => { it('should show the spinner', () => { isBusySubject.next(true); - fixture.detectChanges(); + detectChanges(); expect(el.query(By.css('cx-spinner'))).toBeTruthy(); }); }); @@ -135,14 +162,14 @@ describe('MyAccountV2ProfileComponent', () => { it('should enable the submit button', () => { component.form.enable(); component.onEdit(); - fixture.detectChanges(); + detectChanges(); const submitBtn = el.query(By.css('.btn-primary')); expect(submitBtn.nativeElement.disabled).toBeFalsy(); }); it('should not show the spinner', () => { isBusySubject.next(false); - fixture.detectChanges(); + detectChanges(); expect(el.query(By.css('cx-spinner'))).toBeNull(); }); }); @@ -150,7 +177,7 @@ describe('MyAccountV2ProfileComponent', () => { describe('idle - display', () => { it('should hide the submit button', () => { component.ngOnInit(); - fixture.detectChanges(); + detectChanges(); expect(el.query(By.css('form'))).toBeNull(); }); }); @@ -158,8 +185,8 @@ describe('MyAccountV2ProfileComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { component.onEdit(); - fixture.detectChanges(); - const request = spyOn(component, 'onSubmit'); + detectChanges(); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); @@ -172,8 +199,9 @@ describe('MyAccountV2ProfileComponent', () => { it('when cancel is called. submit button is not visible', () => { component.form.enable(); - fixture.detectChanges(); + detectChanges(); component.cancelEdit(); + detectChanges(); const submitBtn = el.query(By.css('button.btn-primary')); expect(submitBtn).toBeNull(); }); @@ -190,7 +218,7 @@ describe('MyAccountV2ProfileComponent', () => { beforeEach(() => { toggleController.set('a11yFormFieldSectionLegend', true); component.onEdit(); - fixture.detectChanges(); + detectChanges(); }); it('should render a fieldset with a visible legend', () => { @@ -206,7 +234,7 @@ describe('MyAccountV2ProfileComponent', () => { beforeEach(() => { toggleController.set('a11yFormFieldSectionLegend', false); component.onEdit(); - fixture.detectChanges(); + detectChanges(); }); it('should render a fieldset', () => { diff --git a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts index a161c807c7d..94eafb61a34 100644 --- a/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts +++ b/feature-libs/user/profile/components/update-profile/my-account-v2-profile.component.ts @@ -24,10 +24,10 @@ import { } from '@spartacus/core'; import { FormErrorsComponent, - getPageTitle, NgSelectA11yDirective, SpinnerComponent, TruncationTooltipDirective, + getPageTitle, } from '@spartacus/storefront'; import { User } from '@spartacus/user/account/root'; import { Title } from '@spartacus/user/profile/root'; diff --git a/feature-libs/user/profile/components/update-profile/update-profile-component.service.spec.ts b/feature-libs/user/profile/components/update-profile/update-profile-component.service.spec.ts index 1bc1bf5d91d..98556817f32 100644 --- a/feature-libs/user/profile/components/update-profile/update-profile-component.service.spec.ts +++ b/feature-libs/user/profile/components/update-profile/update-profile-component.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { @@ -10,7 +11,6 @@ import { FormErrorsModule } from '@spartacus/storefront'; import { UserProfileFacade } from '@spartacus/user/profile/root'; import { EMPTY, of } from 'rxjs'; import { UpdateProfileComponentService } from './update-profile-component.service'; -import createSpy = jasmine.createSpy; const mockUser = { customerId: '123', @@ -20,13 +20,13 @@ const mockUser = { }; class MockUserProfileFacade implements Partial { - get = createSpy('UserProfileFacade.get').and.returnValue(of({})); - getTitles = createSpy('UserProfileFacade.getTitles').and.returnValue(EMPTY); - update = createSpy('UserProfileFacade.update').and.returnValue(of({})); - close = createSpy('UserProfileFacade.close').and.returnValue(EMPTY); + get = vi.fn('UserProfileFacade.get').mockReturnValue(of({})); + getTitles = vi.fn('UserProfileFacade.getTitles').mockReturnValue(EMPTY); + update = vi.fn('UserProfileFacade.update').mockReturnValue(of({})); + close = vi.fn('UserProfileFacade.close').mockReturnValue(EMPTY); } class MockGlobalMessageService { - add = createSpy().and.stub(); + add = vi.fn().mockImplementation(() => {}); } describe('UpdateProfileComponentService', () => { @@ -66,16 +66,16 @@ describe('UpdateProfileComponentService', () => { service['busy$'].next(true); let result; service.isUpdating$.subscribe((value) => (result = value)).unsubscribe(); - expect(result).toBeTrue(); - expect(service.form.disabled).toBeTrue(); + expect(result).toBe(true); + expect(service.form.disabled).toBe(true); }); it('should return false', () => { service['busy$'].next(false); let result; service.isUpdating$.subscribe((value) => (result = value)).unsubscribe(); - expect(result).toBeFalse; - expect(service.form.disabled).toBeFalse(); + expect(result).toBe(false); + expect(service.form.disabled).toBe(false); }); }); @@ -101,7 +101,7 @@ describe('UpdateProfileComponentService', () => { }); it('reset()', () => { - spyOn(service.form, 'reset').and.callThrough(); + vi.spyOn(service.form, 'reset'); service.updateProfile(); expect(service.form.reset).toHaveBeenCalled(); }); diff --git a/feature-libs/user/profile/components/update-profile/update-profile.component.spec.ts b/feature-libs/user/profile/components/update-profile/update-profile.component.spec.ts index ea0d330e5c4..56485c0121d 100644 --- a/feature-libs/user/profile/components/update-profile/update-profile.component.spec.ts +++ b/feature-libs/user/profile/components/update-profile/update-profile.component.spec.ts @@ -1,4 +1,3 @@ -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -6,7 +5,7 @@ import { Directive, Input, } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, UntypedFormControl, @@ -15,7 +14,6 @@ import { import { By } from '@angular/platform-browser'; import { NgSelectModule } from '@ng-select/ng-select'; import { - FeaturesConfig, MockTranslatePipe, MockTranslationService, PageMeta, @@ -23,12 +21,15 @@ import { RoutingService, TranslatePipe, TranslationService, + FeatureDirective, } from '@spartacus/core'; import { FormErrorsModule, NgSelectA11yDirective, SpinnerComponent, + TruncationTooltipDirective, } from '@spartacus/storefront'; +import { MockFeatureDirective } from 'core-libs/storefront/shared/test/mock-feature-directive'; import { MockFeatureTogglesController, provideMockFeatureToggles, @@ -36,11 +37,11 @@ import { import { BehaviorSubject, of } from 'rxjs'; import { UpdateProfileComponentService } from './update-profile-component.service'; import { UpdateProfileComponent } from './update-profile.component'; -import createSpy = jasmine.createSpy; @Component({ selector: 'cx-spinner', template: `
spinner
`, + imports: [], }) class MockCxSpinnerComponent {} @@ -62,7 +63,7 @@ class MockUpdateProfileService lastName: new UntypedFormControl(), }); isUpdating$ = isBusySubject; - updateProfile = createSpy().and.stub(); + updateProfile = vi.fn().mockImplementation(() => {}); } class MockRoutingService implements Partial { @@ -85,28 +86,14 @@ describe('UpdateProfileComponent', () => { let service: UpdateProfileComponentService; - beforeEach(waitForAsync(() => { + beforeEach(async () => { TestBed.configureTestingModule({ - imports: [ - CommonModule, - ReactiveFormsModule, - FormErrorsModule, - NgSelectModule, - UpdateProfileComponent, - MockCxSpinnerComponent, - MockNgSelectA11yDirective, - ], + imports: [ReactiveFormsModule, FormErrorsModule, UpdateProfileComponent], providers: [ { provide: UpdateProfileComponentService, useClass: MockUpdateProfileService, }, - { - provide: FeaturesConfig, - useValue: { - features: { level: '5.2' }, - }, - }, { provide: RoutingService, useClass: MockRoutingService }, { provide: TranslationService, useClass: MockTranslationService }, { provide: PageMetaService, useClass: MockPageMetaService }, @@ -115,19 +102,26 @@ describe('UpdateProfileComponent', () => { }) .overrideComponent(UpdateProfileComponent, { remove: { - imports: [TranslatePipe, SpinnerComponent, NgSelectA11yDirective], + imports: [ + TranslatePipe, + SpinnerComponent, + NgSelectA11yDirective, + TruncationTooltipDirective, + FeatureDirective, + ], }, add: { imports: [ MockTranslatePipe, MockCxSpinnerComponent, MockNgSelectA11yDirective, + MockFeatureDirective, ], changeDetection: ChangeDetectionStrategy.Default, }, }) .compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(UpdateProfileComponent); @@ -178,7 +172,7 @@ describe('UpdateProfileComponent', () => { describe('Form Interactions', () => { it('should call onSubmit() method on submit', () => { - const request = spyOn(component, 'onSubmit'); + const request = vi.spyOn(component, 'onSubmit'); const form = el.query(By.css('form')); form.triggerEventHandler('submit', null); expect(request).toHaveBeenCalled(); @@ -190,7 +184,7 @@ describe('UpdateProfileComponent', () => { }); it('should navigate to home on cancel', () => { - spyOn(routingService, 'go'); + vi.spyOn(routingService, 'go'); const cancelBtn = el.query(By.css('button.btn-secondary')); cancelBtn.triggerEventHandler('click'); expect(routingService.go).toHaveBeenCalledWith({ cxRoute: 'home' }); diff --git a/feature-libs/user/profile/components/update-profile/update-profile.component.ts b/feature-libs/user/profile/components/update-profile/update-profile.component.ts index 09f2c287574..409beb591c0 100644 --- a/feature-libs/user/profile/components/update-profile/update-profile.component.ts +++ b/feature-libs/user/profile/components/update-profile/update-profile.component.ts @@ -27,10 +27,10 @@ import { FormErrorsComponent, FormRequiredAsterisksComponent, FormRequiredLegendComponent, - getPageTitle, NgSelectA11yDirective, SpinnerComponent, TruncationTooltipDirective, + getPageTitle, } from '@spartacus/storefront'; import { Title } from '@spartacus/user/profile/root'; import { Observable } from 'rxjs'; diff --git a/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts b/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts index 39b43375b7f..47fb1a45028 100644 --- a/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts +++ b/feature-libs/user/profile/core/connectors/user-profile.connector.spec.ts @@ -1,24 +1,24 @@ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { UserSignUp } from '@spartacus/user/profile/root'; import { of } from 'rxjs'; import { UserProfileAdapter } from './user-profile.adapter'; import { UserProfileConnector } from './user-profile.connector'; -import createSpy = jasmine.createSpy; class MockUserAdapter implements UserProfileAdapter { - update = createSpy('update').and.returnValue(of({})); - register = createSpy('register').and.callFake((userId) => of(userId)); - registerGuest = createSpy('registerGuest').and.callFake((userId) => - of(userId) - ); - close = createSpy('remove').and.returnValue(of({})); - requestForgotPasswordEmail = createSpy( - 'requestForgotPasswordEmail' - ).and.returnValue(of({})); - resetPassword = createSpy('resetPassword').and.returnValue(of({})); - updateEmail = createSpy('updateEmail').and.returnValue(of({})); - updatePassword = createSpy('updatePassword').and.returnValue(of({})); - loadTitles = createSpy('loadTitles').and.returnValue(of([])); + update = vi.fn('update').mockReturnValue(of({})); + register = vi.fn('register').mockImplementation((userId) => of(userId)); + registerGuest = vi + .fn('registerGuest') + .mockImplementation((userId) => of(userId)); + close = vi.fn('remove').mockReturnValue(of({})); + requestForgotPasswordEmail = vi + .fn('requestForgotPasswordEmail') + .mockReturnValue(of({})); + resetPassword = vi.fn('resetPassword').mockReturnValue(of({})); + updateEmail = vi.fn('updateEmail').mockReturnValue(of({})); + updatePassword = vi.fn('updatePassword').mockReturnValue(of({})); + loadTitles = vi.fn('loadTitles').mockReturnValue(of([])); } describe('UserConnector', () => { diff --git a/feature-libs/user/profile/core/facade/user-email.service.spec.ts b/feature-libs/user/profile/core/facade/user-email.service.spec.ts index 313f864c4a5..cf6b9d8bda4 100644 --- a/feature-libs/user/profile/core/facade/user-email.service.spec.ts +++ b/feature-libs/user/profile/core/facade/user-email.service.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { OCC_USER_ID_CURRENT, UserIdService } from '@spartacus/core'; import { UserProfileConnector } from '@spartacus/user/profile/core'; import { Observable, of } from 'rxjs'; import { UserEmailService } from './user-email.service'; -import createSpy = jasmine.createSpy; class MockUserIdService implements Partial { takeUserId(): Observable { @@ -12,10 +12,12 @@ class MockUserIdService implements Partial { } class MockUserProfileConnector implements Partial { - updateEmail = createSpy().and.callFake( - (_userId: string, _currentPassword: string, _newUserId: string) => - of(undefined) - ); + updateEmail = vi + .fn() + .mockImplementation( + (_userId: string, _currentPassword: string, _newUserId: string) => + of(undefined) + ); } describe('UserEmailService', () => { diff --git a/feature-libs/user/profile/core/facade/user-password.service.spec.ts b/feature-libs/user/profile/core/facade/user-password.service.spec.ts index b10e425a35b..88c2c3afaa4 100644 --- a/feature-libs/user/profile/core/facade/user-password.service.spec.ts +++ b/feature-libs/user/profile/core/facade/user-password.service.spec.ts @@ -1,9 +1,9 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { OCC_USER_ID_CURRENT, UserIdService } from '@spartacus/core'; import { Observable, of } from 'rxjs'; import { UserPasswordService } from './user-password.service'; import { UserProfileConnector } from '@spartacus/user/profile/core'; -import createSpy = jasmine.createSpy; class MockUserIdService implements Partial { takeUserId(): Observable { @@ -12,9 +12,9 @@ class MockUserIdService implements Partial { } class MockUserProfileConnector implements Partial { - updatePassword = createSpy().and.returnValue(of(undefined)); - requestForgotPasswordEmail = createSpy().and.returnValue(of(undefined)); - resetPassword = createSpy().and.returnValue(of(undefined)); + updatePassword = vi.fn().mockReturnValue(of(undefined)); + requestForgotPasswordEmail = vi.fn().mockReturnValue(of(undefined)); + resetPassword = vi.fn().mockReturnValue(of(undefined)); } describe('UserPasswordService', () => { diff --git a/feature-libs/user/profile/core/facade/user-profile.service.spec.ts b/feature-libs/user/profile/core/facade/user-profile.service.spec.ts index faf737ff522..f17adacef81 100644 --- a/feature-libs/user/profile/core/facade/user-profile.service.spec.ts +++ b/feature-libs/user/profile/core/facade/user-profile.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { AuthService, @@ -6,15 +7,13 @@ import { } from '@spartacus/core'; import { User, UserAccountFacade } from '@spartacus/user/account/root'; import { Title } from '@spartacus/user/profile/root'; -import { Observable, of } from 'rxjs'; +import { Observable, firstValueFrom, of } from 'rxjs'; import { UserProfileConnector } from '../connectors/user-profile.connector'; import { UserProfileService } from './user-profile.service'; -import createSpy = jasmine.createSpy; - class MockUserProfileConnector implements Partial { - update = createSpy().and.returnValue(of(undefined)); - getTitles = createSpy().and.returnValue( + update = vi.fn().mockReturnValue(of(undefined)); + getTitles = vi.fn().mockReturnValue( of([ { code: 't1', name: 't1' }, { code: 't2', name: 't2' }, @@ -25,11 +24,11 @@ class MockUserProfileConnector implements Partial { const testUser = { uid: 'testUser' }; class MockUserAccountFacade implements Partial { - get = createSpy().and.returnValue(of(testUser)); + get = vi.fn().mockReturnValue(of(testUser)); } class MockAuthService implements Partial { - loginWithCredentials = createSpy().and.returnValue(Promise.resolve()); + loginWithCredentials = vi.fn().mockReturnValue(Promise.resolve()); } class MockUserIdService implements Partial { @@ -67,11 +66,9 @@ describe('UserProfileService', () => { } )); - it('should be able to get user data', (done) => { - service.get().subscribe((data) => { - expect(data).toEqual(testUser); - done(); - }); + it('should be able to get user data', async () => { + const data = await firstValueFrom(service.get()); + expect(data).toEqual(testUser); }); it('should update user profile', () => { diff --git a/feature-libs/user/profile/core/facade/user-register.service.spec.ts b/feature-libs/user/profile/core/facade/user-register.service.spec.ts index 42ed97c39b0..3c687ec1dd0 100644 --- a/feature-libs/user/profile/core/facade/user-register.service.spec.ts +++ b/feature-libs/user/profile/core/facade/user-register.service.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { inject, TestBed } from '@angular/core/testing'; import { AuthService, @@ -14,26 +15,25 @@ import { Observable, of } from 'rxjs'; import { UserProfileService } from './user-profile.service'; import { UserRegisterService } from './user-register.service'; import { provideMockFeatureToggles } from 'core-libs/core/src/features-config/feature-toggles/testing'; -import createSpy = jasmine.createSpy; class MockUserProfileService implements Partial { get(): Observable { return of({ uid: OCC_USER_ID_CURRENT }); } - getTitles = createSpy().and.returnValue(of([])); + getTitles = vi.fn().mockReturnValue(of([])); } class MockUserProfileConnector implements Partial { - register = createSpy().and.callFake((user) => of(user)); - registerGuest = createSpy().and.callFake((uid, _password) => of({ uid })); + register = vi.fn().mockImplementation((user) => of(user)); + registerGuest = vi.fn().mockImplementation((uid, _password) => of({ uid })); } class MockAuthService implements Partial { - loginWithCredentials = createSpy().and.returnValue(Promise.resolve()); + loginWithCredentials = vi.fn().mockReturnValue(Promise.resolve()); } class MockRoutingService implements Partial { - go = createSpy().and.returnValue(Promise.resolve()); + go = vi.fn().mockReturnValue(Promise.resolve()); } const mockFeatureToggles: FeatureToggles = { diff --git a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts index 3aea852b06e..b3bf8f24369 100644 --- a/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts +++ b/feature-libs/user/profile/occ/adapters/occ-user-profile.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; import { HttpTestingController, provideHttpClientTesting, @@ -121,10 +122,10 @@ describe('OccUserProfileAdapter', () => { httpMock = TestBed.inject(HttpTestingController); converter = TestBed.inject(ConverterService); occEndpointsService = TestBed.inject(OccEndpointsService); - spyOn(converter, 'pipeableMany').and.callThrough(); - spyOn(converter, 'pipeable').and.callThrough(); - spyOn(converter, 'convert').and.callThrough(); - spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(converter, 'pipeableMany'); + vi.spyOn(converter, 'pipeable'); + vi.spyOn(converter, 'convert'); + vi.spyOn(occEndpointsService, 'buildUrl'); }); afterEach(() => { diff --git a/feature-libs/user/profile/root/services/user-currency-preference-saver.service.spec.ts b/feature-libs/user/profile/root/services/user-currency-preference-saver.service.spec.ts index 5aca1d50d34..fc6c8515518 100644 --- a/feature-libs/user/profile/root/services/user-currency-preference-saver.service.spec.ts +++ b/feature-libs/user/profile/root/services/user-currency-preference-saver.service.spec.ts @@ -14,18 +14,18 @@ import { } from '@spartacus/core'; import { UserAccountConfig } from '@spartacus/user/account/root'; import { Subject, of } from 'rxjs'; +import { vi } from 'vitest'; import { UserProfileFacade } from '../facade/user-profile.facade'; import { UserCurrencyPreferenceSaverService } from './user-currency-preference-saver.service'; -import createSpy = jasmine.createSpy; const mockEventStream$ = new Subject(); class MockEventService implements Partial { - get = createSpy().and.returnValue(mockEventStream$.asObservable()); + get = vi.fn().mockReturnValue(mockEventStream$.asObservable()); } class MockUserProfileFacade implements Partial { - update = createSpy().and.returnValue(of({})); + update = vi.fn().mockReturnValue(of({})); } describe('UserCurrencyPreferenceSaverService', () => { @@ -39,7 +39,7 @@ describe('UserCurrencyPreferenceSaverService', () => { { provide: UserProfileFacade, useClass: MockUserProfileFacade }, { provide: UserIdService, - useValue: { getUserId: createSpy().and.returnValue(of(userId)) }, + useValue: { getUserId: vi.fn().mockReturnValue(of(userId)) }, }, { provide: UserAccountConfig, diff --git a/feature-libs/user/project.json b/feature-libs/user/project.json index 12a69bbb275..d4b24d29dd6 100644 --- a/feature-libs/user/project.json +++ b/feature-libs/user/project.json @@ -17,13 +17,11 @@ } } }, - "test": { - "executor": "@angular-devkit/build-angular:karma", + "test-vitest": { + "executor": "nx:run-commands", "options": { - "main": "feature-libs/user/test.ts", - "tsConfig": "feature-libs/user/tsconfig.spec.json", - "polyfills": ["zone.js", "zone.js/testing"], - "karmaConfig": "feature-libs/user/karma.conf.js" + "command": "npx vitest run --config vitest.config.ts", + "cwd": "{projectRoot}" } }, "test-jest": { diff --git a/feature-libs/user/test.ts b/feature-libs/user/test.ts deleted file mode 100644 index 36434c2919b..00000000000 --- a/feature-libs/user/test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 SAP Spartacus team - * - * SPDX-License-Identifier: Apache-2.0 - */ - -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import { NgModule, provideZoneChangeDetection } from '@angular/core'; -import { getTestBed } from '@angular/core/testing'; -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; - -// Angular 21 introduced a change that causes NG0100 errors in Karma tests. -// See: https://github.com/angular/angular-cli/issues/32047 -// Angular fixed this for built-in test.ts: https://github.com/angular/angular-cli/pull/32049 -// Since we use a custom test.ts, we must manually provide zone change detection. -@NgModule({ - providers: [provideZoneChangeDetection()], -}) -class ZoneChangeDetectionModule {} - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - [BrowserTestingModule, ZoneChangeDetectionModule], - platformBrowserTesting(), - { - teardown: { destroyAfterEach: false }, - } -); diff --git a/feature-libs/user/tsconfig.spec.json b/feature-libs/user/tsconfig.spec.json index 75f96e789ee..211793fbe5a 100644 --- a/feature-libs/user/tsconfig.spec.json +++ b/feature-libs/user/tsconfig.spec.json @@ -3,10 +3,32 @@ "compilerOptions": { "outDir": "../../out-tsc/spec", "module": "preserve", - "types": ["jasmine", "node"], "strict": false, - "moduleResolution": "bundler" + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "moduleResolution": "bundler", + "allowJs": true, + "paths": { + "@spartacus/storefront/testing/mock-feature-directive": [ + "../../core-libs/storefront/shared/test/mock-feature-directive.ts" + ], + "@spartacus/storefront/testing/mock-feature-level-directive": [ + "../../core-libs/storefront/shared/test/mock-feature-level-directive.ts" + ], + "@spartacus/core/testing/process-reducers": [ + "../../core-libs/core/src/process/store/reducers/index.ts" + ] + } }, - "files": ["test.ts"], - "include": ["**/*.spec.ts", "**/*.d.ts"] + "files": ["../../testing/setup-vitest.ts"], + "include": [ + "**/*.ts", + "../../core-libs/core/src/**/*.ts", + "../../core-libs/storefront/**/*.ts" + ] } diff --git a/feature-libs/user/vitest.config.ts b/feature-libs/user/vitest.config.ts new file mode 100644 index 00000000000..77b2fe11cbd --- /dev/null +++ b/feature-libs/user/vitest.config.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import angular from '@analogjs/vite-plugin-angular'; +import { defineConfig } from 'vitest/config'; + +const root = `${import.meta.dirname}/../..`; + +export default defineConfig({ + root: import.meta.dirname, + plugins: [angular(), nxViteTsPaths()], + resolve: { + alias: { + 'core-libs/storefront/shared/test/mock-feature-directive': `${root}/core-libs/storefront/shared/test/mock-feature-directive.ts`, + 'core-libs/core/src/features-config/feature-toggles/testing': `${root}/core-libs/core/src/features-config/feature-toggles/testing/index.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/mock-url.pipe.ts`, + 'core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module': `${root}/core-libs/core/src/routing/configurable-routes/url-translation/testing/url-testing.module.ts`, + }, + }, + test: { + pool: 'forks', + watch: false, + globals: true, + environment: 'jsdom', + setupFiles: ['../../testing/setup-vitest.ts'], + include: ['**/*.spec.ts'], + typecheck: { + tsconfig: `${import.meta.dirname}/tsconfig.spec.json`, + }, + coverage: { + provider: 'v8', + reporter: ['lcov'], + reportsDirectory: `${import.meta.dirname}/../../coverage/user`, + exclude: [ + '**/public_api.ts', + '**/index.ts', + '**/*.module.ts', + '../../testing/setup-vitest.ts', + ], + thresholds: { + statements: 90, + lines: 90, + branches: 80, + functions: 90, + }, + }, + reporters: [ + 'default', + [ + 'junit', + { + outputFile: `${import.meta.dirname}/../../unit-tests-reports/unit-test-user.xml`, + }, + ], + ], + }, +}); diff --git a/feature-libs/user/wishlist/core/connectors/user-wishlist.adapter.spec.ts b/feature-libs/user/wishlist/core/connectors/user-wishlist.adapter.spec.ts index d92e565b253..7bacb9310fc 100644 --- a/feature-libs/user/wishlist/core/connectors/user-wishlist.adapter.spec.ts +++ b/feature-libs/user/wishlist/core/connectors/user-wishlist.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* * SPDX-FileCopyrightText: 2026 SAP Spartacus team * @@ -69,7 +70,7 @@ describe('UserWishlistAdapter', () => { }); it('should accept userId and return an Observable', () => { - spyOn(adapter, 'getWishlist').and.returnValue(of(mockWishlist)); + vi.spyOn(adapter, 'getWishlist').mockReturnValue(of(mockWishlist)); let result: Wishlist | undefined; adapter.getWishlist(MOCK_USER_ID).subscribe((wl) => (result = wl)); @@ -79,7 +80,7 @@ describe('UserWishlistAdapter', () => { }); it('should forward the userId argument to the implementation', () => { - const spy = spyOn(adapter, 'getWishlist').and.callThrough(); + const spy = vi.spyOn(adapter, 'getWishlist'); adapter.getWishlist(MOCK_USER_ID).subscribe(); expect(spy).toHaveBeenCalledWith(MOCK_USER_ID); }); @@ -91,7 +92,7 @@ describe('UserWishlistAdapter', () => { }); it('should accept userId, wishlistId, productCode and return Observable', () => { - spyOn(adapter, 'addEntry').and.returnValue(of(mockEntry)); + vi.spyOn(adapter, 'addEntry').mockReturnValue(of(mockEntry)); let result: WishlistEntry | undefined; adapter @@ -107,7 +108,7 @@ describe('UserWishlistAdapter', () => { }); it('should forward all three arguments to the implementation', () => { - const spy = spyOn(adapter, 'addEntry').and.callThrough(); + const spy = vi.spyOn(adapter, 'addEntry'); adapter .addEntry(MOCK_USER_ID, MOCK_WISHLIST_ID, MOCK_PRODUCT_CODE) .subscribe(); @@ -125,7 +126,7 @@ describe('UserWishlistAdapter', () => { }); it('should accept userId, wishlistId, entryId and return Observable', () => { - spyOn(adapter, 'removeEntry').and.returnValue(of(undefined as void)); + vi.spyOn(adapter, 'removeEntry').mockReturnValue(of(undefined as void)); let called = false; adapter @@ -141,7 +142,7 @@ describe('UserWishlistAdapter', () => { }); it('should forward all three arguments to the implementation', () => { - const spy = spyOn(adapter, 'removeEntry').and.callThrough(); + const spy = vi.spyOn(adapter, 'removeEntry'); adapter .removeEntry(MOCK_USER_ID, MOCK_WISHLIST_ID, MOCK_ENTRY_ID) .subscribe(); diff --git a/feature-libs/user/wishlist/core/connectors/user-wishlist.connector.spec.ts b/feature-libs/user/wishlist/core/connectors/user-wishlist.connector.spec.ts index 3c586e68610..ea1f3bd2688 100644 --- a/feature-libs/user/wishlist/core/connectors/user-wishlist.connector.spec.ts +++ b/feature-libs/user/wishlist/core/connectors/user-wishlist.connector.spec.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { Wishlist, WishlistEntry } from '@spartacus/user/wishlist/root'; @@ -12,7 +13,7 @@ import { UserWishlistConnector } from './user-wishlist.connector'; describe('UserWishlistConnector', () => { let connector: UserWishlistConnector; - let adapter: jasmine.SpyObj; + let adapter: any; const MOCK_USER_ID = 'user-001'; const MOCK_WISHLIST_ID = 'wishlist-uuid-123'; @@ -36,15 +37,15 @@ describe('UserWishlistConnector', () => { }; beforeEach(() => { - adapter = jasmine.createSpyObj('UserWishlistAdapter', [ - 'getWishlist', - 'addEntry', - 'removeEntry', - ]); - - adapter.getWishlist.and.returnValue(of(mockWishlist)); - adapter.addEntry.and.returnValue(of(mockEntry)); - adapter.removeEntry.and.returnValue(of(undefined as void)); + adapter = { + getWishlist: vi.fn(), + addEntry: vi.fn(), + removeEntry: vi.fn(), + }; + + adapter.getWishlist.mockReturnValue(of(mockWishlist)); + adapter.addEntry.mockReturnValue(of(mockEntry)); + adapter.removeEntry.mockReturnValue(of(undefined as void)); TestBed.configureTestingModule({ providers: [ diff --git a/feature-libs/user/wishlist/occ/adapters/occ-user-wishlist.adapter.spec.ts b/feature-libs/user/wishlist/occ/adapters/occ-user-wishlist.adapter.spec.ts index 3b2042262e0..28dece89d3f 100644 --- a/feature-libs/user/wishlist/occ/adapters/occ-user-wishlist.adapter.spec.ts +++ b/feature-libs/user/wishlist/occ/adapters/occ-user-wishlist.adapter.spec.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest'; /* * SPDX-FileCopyrightText: 2026 SAP Spartacus team * @@ -87,7 +88,7 @@ describe('OccUserWishlistAdapter', () => { httpMock = TestBed.inject(HttpTestingController); occEndpointsService = TestBed.inject(OccEndpointsService); - spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + vi.spyOn(occEndpointsService, 'buildUrl'); }); afterEach(() => {