mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 1853: feat(return-process): add getReceiptItemQuantity helper and related tests
feat(return-process): add getReceiptItemQuantity helper and related tests Ref: #5156
This commit is contained in:
committed by
Nino Righi
parent
a37201ef33
commit
61ce9940c9
@@ -0,0 +1,36 @@
|
||||
import { getReceiptItemQuantity } from './get-receipt-item-quantity.helper';
|
||||
|
||||
describe('getItemQuantity', () => {
|
||||
it('should return item quantity when not present in quantity map', () => {
|
||||
// Arrange
|
||||
const item = { id: 123, quantity: { quantity: 5 } };
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
it('should return 1 as default when item quantity is 0', () => {
|
||||
// Arrange
|
||||
const item = { id: 123, quantity: { quantity: 0 } };
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 1 as default when item quantity is null', () => {
|
||||
// Arrange
|
||||
const item = { id: 123, quantity: { quantity: null as unknown as number } };
|
||||
|
||||
// Act
|
||||
const result = getReceiptItemQuantity(item);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export function getReceiptItemQuantity(item: {
|
||||
id: number;
|
||||
quantity?: { quantity: number };
|
||||
}): number {
|
||||
return item.quantity?.quantity || 1; // Default to 1 if not specified
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from './eligible-for-return.helper';
|
||||
export * from './get-next-question.helper';
|
||||
export * from './get-receipt-item-action.helper';
|
||||
export * from './get-receipt-item-product-category.helper';
|
||||
export * from './get-receipt-item-quantity.helper';
|
||||
export * from './get-return-info.helper';
|
||||
export * from './get-return-process-questions.helper';
|
||||
export * from './get-tolino-questions.helper';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { patchState } from '@ngrx/signals';
|
||||
import { unprotected } from '@ngrx/signals/testing';
|
||||
import { Receipt, ReceiptItem } from '../models';
|
||||
import { addEntities } from '@ngrx/signals/entities';
|
||||
import { ProductCategory } from '../questions';
|
||||
|
||||
describe('ReturnDetailsStore', () => {
|
||||
const createService = createServiceFactory({
|
||||
@@ -25,10 +26,34 @@ describe('ReturnDetailsStore', () => {
|
||||
const spectator = createService();
|
||||
|
||||
const receiptItems = [
|
||||
{ id: 1 } as ReceiptItem,
|
||||
{ id: 2 } as ReceiptItem,
|
||||
{ id: 3 } as ReceiptItem,
|
||||
{ id: 4 } as ReceiptItem,
|
||||
{
|
||||
id: 1,
|
||||
quantity: { quantity: 1 },
|
||||
features: {
|
||||
category: ProductCategory.BookCalendar,
|
||||
} as { [key: string]: ProductCategory },
|
||||
} as ReceiptItem,
|
||||
{
|
||||
id: 2,
|
||||
quantity: { quantity: 2 },
|
||||
features: {
|
||||
category: ProductCategory.ElektronischeGeraete,
|
||||
} as { [key: string]: ProductCategory },
|
||||
} as ReceiptItem,
|
||||
{
|
||||
id: 3,
|
||||
quantity: { quantity: 3 },
|
||||
features: {
|
||||
category: ProductCategory.SpielwarenPuzzle,
|
||||
} as { [key: string]: ProductCategory },
|
||||
} as ReceiptItem,
|
||||
{
|
||||
id: 4,
|
||||
quantity: { quantity: 4 },
|
||||
features: {
|
||||
category: ProductCategory.TonDatentraeger,
|
||||
} as { [key: string]: ProductCategory },
|
||||
} as ReceiptItem,
|
||||
];
|
||||
|
||||
const receiptsEntities = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, effect, inject, resource, untracked } from '@angular/core';
|
||||
import { computed, inject, resource, untracked } from '@angular/core';
|
||||
import {
|
||||
CanReturn,
|
||||
ProductCategory,
|
||||
@@ -19,11 +19,13 @@ import {
|
||||
import { setEntity, withEntities, entityConfig } from '@ngrx/signals/entities';
|
||||
import {
|
||||
canReturnReceiptItem,
|
||||
getReceiptItemQuantity,
|
||||
getReceiptItemProductCategory,
|
||||
receiptItemHasCategory,
|
||||
} from '../helpers/return-process';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
interface ReturnDetailsState {
|
||||
_storageId: number | undefined;
|
||||
@@ -84,8 +86,41 @@ export const ReturnDetailsStore = signalStore(
|
||||
.receiptsEntities()
|
||||
.map((receipt) => receipt.items)
|
||||
.flat()
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
.map((container) => container.data!),
|
||||
.map((container) => {
|
||||
const item = container.data;
|
||||
if (!item) {
|
||||
const err = new Error('Item data is undefined');
|
||||
store._logger.error('Item data is undefined', err, () => ({
|
||||
item: container,
|
||||
}));
|
||||
throw err;
|
||||
}
|
||||
|
||||
const itemData = clone(item);
|
||||
|
||||
const quantityMap = store.selectedQuantity();
|
||||
|
||||
if (quantityMap[itemData.id]) {
|
||||
itemData.quantity = { quantity: quantityMap[itemData.id] };
|
||||
} else {
|
||||
const quantity = getReceiptItemQuantity(itemData);
|
||||
if (!itemData.quantity) {
|
||||
itemData.quantity = { quantity };
|
||||
} else {
|
||||
itemData.quantity.quantity = quantity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemData.features) {
|
||||
itemData.features = {};
|
||||
}
|
||||
|
||||
itemData.features['category'] =
|
||||
store.selectedProductCategory()[itemData.id] ||
|
||||
getReceiptItemProductCategory(itemData);
|
||||
|
||||
return itemData;
|
||||
}),
|
||||
),
|
||||
})),
|
||||
withComputed((store) => ({
|
||||
@@ -116,9 +151,7 @@ export const ReturnDetailsStore = signalStore(
|
||||
}),
|
||||
selectedItems: computed(() => {
|
||||
const selectedIds = store.selectedItemIds();
|
||||
const items = store
|
||||
.receiptsEntities()
|
||||
.flatMap((receipt) => receipt.items.map((item) => item.data!));
|
||||
const items = store.items();
|
||||
return items.filter((item) => selectedIds.includes(item.id));
|
||||
}),
|
||||
})),
|
||||
@@ -186,10 +219,9 @@ export const ReturnDetailsStore = signalStore(
|
||||
}),
|
||||
getItems: (receiptId: () => number) =>
|
||||
computed(() => {
|
||||
const items = store.items();
|
||||
const id = receiptId();
|
||||
const entities = store.receiptsEntityMap();
|
||||
const receipt = entities[id];
|
||||
return receipt?.items.map((item) => item.data!);
|
||||
return items.filter((item) => item.receipt?.id === id);
|
||||
}),
|
||||
getSelectableItems: (receiptId: () => number) =>
|
||||
computed(() =>
|
||||
@@ -202,21 +234,6 @@ export const ReturnDetailsStore = signalStore(
|
||||
const selectedIds = store.selectedItemIds();
|
||||
return selectedIds.includes(item().id);
|
||||
}),
|
||||
getItemQuantity: (item: () => ReceiptItem) =>
|
||||
computed(() => {
|
||||
const itemData = item();
|
||||
return (
|
||||
store.selectedQuantity()[itemData.id] || itemData.quantity.quantity
|
||||
);
|
||||
}),
|
||||
getProductCategory: (item: () => ReceiptItem) =>
|
||||
computed(() => {
|
||||
const itemData = item();
|
||||
return (
|
||||
store.selectedProductCategory()[itemData.id] ||
|
||||
getReceiptItemProductCategory(itemData)
|
||||
);
|
||||
}),
|
||||
isSelectable: (receiptItem: () => ReceiptItem) =>
|
||||
computed(() => {
|
||||
const item = receiptItem();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ReturnDetailsHeaderComponent } from './return-details-header.component';
|
||||
import { Buyer } from '@isa/oms/data-access';
|
||||
import { Receipt, ReturnDetailsStore } from '@isa/oms/data-access';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
describe('ReturnDetailsHeaderComponent', () => {
|
||||
let spectator: Spectator<ReturnDetailsHeaderComponent>;
|
||||
@@ -8,17 +9,32 @@ describe('ReturnDetailsHeaderComponent', () => {
|
||||
component: ReturnDetailsHeaderComponent,
|
||||
});
|
||||
|
||||
let buyerMock: Buyer;
|
||||
const getReceiptMock = signal<Receipt>({
|
||||
id: 12345,
|
||||
items: [],
|
||||
buyer: {
|
||||
reference: { id: 12345 },
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
buyerNumber: '123456',
|
||||
},
|
||||
});
|
||||
|
||||
const storeMock = {
|
||||
getReceipt: () => getReceiptMock,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
buyerMock = {
|
||||
buyerNumber: '12345',
|
||||
};
|
||||
|
||||
spectator = createComponent({
|
||||
props: {
|
||||
buyer: buyerMock,
|
||||
receiptId: 12345, // Mock receiptId for testing
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
provide: ReturnDetailsStore,
|
||||
useValue: storeMock,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { MockDirective } from 'ng-mocks';
|
||||
|
||||
import { ReceiptItem, ReturnDetailsService } from '@isa/oms/data-access';
|
||||
import {
|
||||
ReceiptItem,
|
||||
ReturnDetailsService,
|
||||
ReturnDetailsStore,
|
||||
} from '@isa/oms/data-access';
|
||||
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
// Helper function to create mock ReceiptItem data
|
||||
const createMockItem = (
|
||||
@@ -16,6 +21,7 @@ const createMockItem = (
|
||||
): ReceiptItem =>
|
||||
({
|
||||
id: 123,
|
||||
receiptNumber: 'R-123456', // Add the required receiptNumber property
|
||||
quantity: { quantity: 1 },
|
||||
price: {
|
||||
value: { value: 19.99, currency: 'EUR' },
|
||||
@@ -40,9 +46,31 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
let spectator: Spectator<ReturnDetailsOrderGroupItemControlsComponent>;
|
||||
const mockItemSelectable = createMockItem('1234567890123', true);
|
||||
|
||||
const mockIsSelectable = signal<boolean>(true);
|
||||
const mockGetItemSelectted = signal<boolean>(false);
|
||||
const mockCanReturnResource = {
|
||||
isLoading: signal<boolean>(true),
|
||||
};
|
||||
|
||||
function resetMocks() {
|
||||
mockIsSelectable.set(true);
|
||||
mockGetItemSelectted.set(false);
|
||||
mockCanReturnResource.isLoading.set(true);
|
||||
}
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: ReturnDetailsOrderGroupItemControlsComponent,
|
||||
mocks: [ReturnDetailsService],
|
||||
providers: [
|
||||
{
|
||||
provide: ReturnDetailsStore,
|
||||
useValue: {
|
||||
isSelectable: jest.fn(() => mockIsSelectable),
|
||||
getItemSelected: jest.fn(() => mockGetItemSelectted),
|
||||
canReturnResource: jest.fn(() => mockCanReturnResource),
|
||||
},
|
||||
},
|
||||
],
|
||||
// Spectator automatically stubs standalone dependencies like ItemRowComponent, CheckboxComponent etc.
|
||||
// We don't need deep interaction, just verify the host component renders correctly.
|
||||
// If specific interactions were needed, we could provide mocks or use overrideComponents.
|
||||
@@ -65,12 +93,14 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
spectator = createComponent({
|
||||
props: {
|
||||
item: mockItemSelectable, // Use signal for input
|
||||
selected: false, // Use signal for model
|
||||
receiptId: 123,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetMocks(); // Reset mocks after each test
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
// Arrange
|
||||
spectator.detectChanges(); // Trigger initial render
|
||||
@@ -81,10 +111,9 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
|
||||
it('should display the checkbox when item is selectable', () => {
|
||||
// Arrange
|
||||
// The mock item has canReturn=true and a valid category, which should make it selectable
|
||||
// after the effect executes
|
||||
mockCanReturnResource.isLoading.set(false); // Simulate the resource being ready
|
||||
mockIsSelectable.set(true); // Simulate the item being selectable
|
||||
spectator.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.selectable()).toBe(true);
|
||||
const checkbox = spectator.query(CheckboxComponent);
|
||||
@@ -95,13 +124,9 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
});
|
||||
it('should NOT display the checkbox when item is not selectable', () => {
|
||||
// Arrange
|
||||
// Create a non-returnable item by modifying the features.category to 'unknown'
|
||||
const nonReturnableItem = createMockItem('1234567890123', true);
|
||||
nonReturnableItem.features = { category: 'unknown' };
|
||||
|
||||
// Set the item to trigger the effects which determine selectability
|
||||
spectator.setInput('item', nonReturnableItem);
|
||||
mockIsSelectable.set(false); // Simulate the item not being selectable
|
||||
spectator.detectChanges();
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.selectable()).toBe(false);
|
||||
@@ -110,55 +135,6 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
).not.toExist();
|
||||
expect(spectator.query(CheckboxComponent)).toBeFalsy();
|
||||
});
|
||||
it('should update the selected model when checkbox is clicked', () => {
|
||||
// Arrange
|
||||
spectator.detectChanges(); // This will make the component selectable via the effect
|
||||
|
||||
// Use the component's method directly to toggle selection
|
||||
// This is similar to what happens when a checkbox is clicked
|
||||
spectator.component.selected.set(!spectator.component.selected());
|
||||
spectator.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.selected()).toBe(true);
|
||||
});
|
||||
it('should reflect the initial selected state in the checkbox', () => {
|
||||
// Arrange
|
||||
// First ensure the item is selectable (has a non-unknown category)
|
||||
const selectableItem = createMockItem(
|
||||
'1234567890123',
|
||||
true,
|
||||
'Test Product',
|
||||
'BOOK',
|
||||
);
|
||||
spectator.setInput('item', selectableItem);
|
||||
spectator.setInput('selected', true); // Start selected
|
||||
spectator.detectChanges(); // This triggers the effects that set selectable
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.selected()).toBe(true);
|
||||
expect(spectator.component.selectable()).toBe(true);
|
||||
|
||||
// For a checkbox, we need to check that it exists
|
||||
const checkbox = spectator.query(
|
||||
'input[type="checkbox"][data-what="return-item-checkbox"]',
|
||||
);
|
||||
expect(checkbox).toExist();
|
||||
// With Spectator we can use toHaveProperty for HTML elements
|
||||
expect(checkbox).toHaveProperty('checked', true);
|
||||
});
|
||||
|
||||
it('should be true when actions include canReturn with truthy value', () => {
|
||||
// Arrange
|
||||
const item = createMockItem('0001', true);
|
||||
spectator.setInput('item', item);
|
||||
|
||||
// Act
|
||||
spectator.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.canReturnReceiptItem()).toBe(true);
|
||||
});
|
||||
|
||||
it('should be false when no canReturn action is present', () => {
|
||||
// Arrange
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import { provideLoggerContext } from '@isa/core/logging';
|
||||
import {
|
||||
canReturnReceiptItem,
|
||||
getReceiptItemProductCategory,
|
||||
getReceiptItemQuantity,
|
||||
ProductCategory,
|
||||
ReceiptItem,
|
||||
ReturnDetailsService,
|
||||
@@ -63,14 +65,20 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
|
||||
|
||||
availableCategories = this.#returnDetailsService.availableCategories();
|
||||
|
||||
quantity = this.#store.getItemQuantity(this.item);
|
||||
quantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemQuantity(item);
|
||||
});
|
||||
|
||||
quantityDropdownValues = computed(() => {
|
||||
const itemQuantity = this.item().quantity.quantity;
|
||||
return Array.from({ length: itemQuantity }, (_, i) => i + 1);
|
||||
});
|
||||
|
||||
productCategory = this.#store.getProductCategory(this.item);
|
||||
productCategory = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemProductCategory(item);
|
||||
});
|
||||
|
||||
selectable = this.#store.isSelectable(this.item);
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { createComponentFactory, Spectator, mockProvider } from '@ngneat/spectator/jest';
|
||||
import {
|
||||
createComponentFactory,
|
||||
Spectator,
|
||||
mockProvider,
|
||||
} from '@ngneat/spectator/jest';
|
||||
import { FilterActionsComponent } from './filter-actions.component';
|
||||
import { FilterService } from '../core';
|
||||
|
||||
@@ -13,7 +17,6 @@ describe('FilterActionsComponent', () => {
|
||||
{ group: 'other', key: 'key2' },
|
||||
]),
|
||||
commit: jest.fn(),
|
||||
commitInput: jest.fn(),
|
||||
reset: jest.fn(),
|
||||
resetInput: jest.fn(),
|
||||
}),
|
||||
@@ -51,7 +54,7 @@ describe('FilterActionsComponent', () => {
|
||||
spectator.setInput('inputKey', 'key1');
|
||||
spectator.component.onApply();
|
||||
|
||||
expect(filterService.commitInput).toHaveBeenCalledWith('key1');
|
||||
expect(filterService.commit).toHaveBeenCalled();
|
||||
expect(appliedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -74,7 +77,7 @@ describe('FilterActionsComponent', () => {
|
||||
spectator.setInput('inputKey', 'key1');
|
||||
spectator.component.onReset();
|
||||
|
||||
expect(filterService.resetInput).toHaveBeenCalledWith('key1');
|
||||
expect(filterService.resetInput).toHaveBeenCalledWith(['key1']);
|
||||
expect(filterService.commit).toHaveBeenCalled();
|
||||
expect(resetedSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user