feat(checkout): add branch selection to reward catalog - Add new select-branch-dropdown library with BranchDropdownComponent and SelectedBranchDropdownComponent for branch selection - Extend DropdownButtonComponent with filter and option subcomponents - Integrate branch selection into reward catalog page - Add BranchesResource for fetching available branches - Update CheckoutMetadataService with branch selection persistence - Add comprehensive tests for dropdown components Related work items: #5464
@isa/checkout/shared/product-info
A comprehensive collection of presentation components for displaying product information, destination details, and stock availability in checkout and rewards workflows.
Overview
The Product Info library provides three specialized components for rendering product-related information in the checkout domain. These components handle the visual presentation layer for reward redemptions, delivery/pickup destinations, and real-time stock availability. Each component is designed as a standalone Angular component with signal-based inputs and optimized change detection.
Table of Contents
- Features
- Quick Start
- Components
- Type Definitions
- Usage Examples
- Styling and Customization
- Architecture Notes
- Testing
- Dependencies
Features
- Product redemption display - Formatted product information with reward points
- Destination visualization - Dynamic display of shipping/pickup/in-store destinations
- Real-time stock info - Asynchronous stock availability with loading states
- Flexible layouts - Horizontal and vertical orientation support
- Signal-based reactivity - Modern Angular signals for optimal performance
- Resource integration - Angular Resource API for efficient data fetching
- E2E test support - Comprehensive
data-whatattributes for automated testing - Tailwind styling - ISA design system integration with custom utilities
- OnPush change detection - Optimized rendering performance
- Type-safe inputs - Strict TypeScript types with discriminated unions
Quick Start
1. Import Components
import { Component } from '@angular/core';
import {
ProductInfoRedemptionComponent,
DestinationInfoComponent,
StockInfoComponent,
type ProductInfoItem,
type StockInfoItem,
} from '@isa/checkout/shared/product-info';
@Component({
selector: 'app-checkout-summary',
template: '...',
imports: [
ProductInfoRedemptionComponent,
DestinationInfoComponent,
StockInfoComponent,
],
})
export class CheckoutSummaryComponent {
// Component logic...
}
2. Basic Product Info Display
@Component({
template: `
<checkout-product-info-redemption
[item]="productItem()"
[orientation]="'vertical'"
/>
`,
imports: [ProductInfoRedemptionComponent],
})
export class RewardItemComponent {
productItem = signal<ProductInfoItem>({
product: {
ean: '9783498007706',
name: 'Die Assistentin',
contributors: 'Wahl, Caroline',
format: 'TB',
formatDetail: 'Taschenbuch (Kartoniert)',
manufacturer: 'Rowohlt Verlag',
publicationDate: '2023-01-15',
},
redemptionPoints: 150,
});
}
3. Destination Display
@Component({
template: `
<checkout-destination-info
[shoppingCartItem]="cartItem()"
[underline]="true"
/>
`,
imports: [DestinationInfoComponent],
})
export class CartItemComponent {
cartItem = signal({
availability: {
estimatedDelivery: {
start: '2024-06-10T00:00:00+02:00',
stop: '2024-06-12T00:00:00+02:00',
},
},
destination: {
data: {
target: ShippingTarget.Delivery,
},
},
features: {
orderType: 'Versand',
},
});
}
4. Stock Information Display
@Component({
template: `
<checkout-stock-info [item]="stockItem()" />
`,
imports: [StockInfoComponent],
})
export class ProductStockComponent {
stockItem = signal<StockInfoItem>({
id: 123456,
catalogAvailability: {
ssc: '999',
sscText: 'Lieferbar in 1-3 Werktagen',
},
});
}
Components
ProductInfoRedemptionComponent
A presentation component for displaying product information with redemption points in reward workflows.
Selector
<checkout-product-info-redemption />
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
item |
ProductInfoItem |
- | ✓ | Product data with redemption points or loyalty information |
orientation |
'horizontal' | 'vertical' |
'vertical' |
✗ | Layout orientation |
Outputs
None - presentation component only.
Computed Properties
| Name | Type | Description |
|---|---|---|
points |
Signal<number> |
Calculated reward points from redemptionPoints or loyalty.value |
Host Bindings
class: Dynamically bound toorientation()for CSS styling
Template Features
- Product Image: Clickable product image with
sharedProductRouterLinkdirective - Contributors: Bold product author/artist display
- Product Name: Responsive typography (body-2 horizontal, subtitle-1 vertical)
- Reward Points: Combined display of points value and "Lesepunkte" label
- Format Information: Delegated to
shared-product-formatcomponent - Metadata: Manufacturer, EAN, and publication date
- E2E Attributes:
data-what="product-image"for automated testing
Example
@Component({
template: `
<checkout-product-info-redemption
[item]="rewardItem()"
[orientation]="'horizontal'"
/>
`,
})
export class RewardCardComponent {
rewardItem = signal<ProductInfoItem>({
product: {
ean: '9783498007706',
name: 'Die Assistentin',
contributors: 'Wahl, Caroline',
format: 'TB',
formatDetail: 'Taschenbuch (Kartoniert)',
manufacturer: 'Rowohlt Verlag',
publicationDate: '2023-01-15',
},
redemptionPoints: 150,
});
}
DestinationInfoComponent
Displays shipping destination, branch information, or estimated delivery dates based on order type.
Selector
<checkout-destination-info />
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
shoppingCartItem |
Pick<ShoppingCartItem, 'availability' | 'destination' | 'features'> |
- | ✓ | Shopping cart item with destination data |
underline |
boolean |
false |
✗ | Add underline styling to destination header |
Outputs
None - presentation component only.
Computed Properties
| Name | Type | Description |
|---|---|---|
orderType |
Signal<OrderType> |
Extracted order type from cart item features |
destinationIcon |
Signal<string> |
Icon name based on order type (delivery, pickup, in-store) |
displayAddress |
Signal<boolean> |
Whether to show address (true for InStore, Pickup, B2B) |
branchContainer |
Signal<EntityReferenceContainer | undefined> |
Branch reference from destination data |
branch |
LinkedSignal<Branch | undefined> |
Resolved branch data from resource or container |
name |
Signal<string> |
Recipient name (customer or branch name) |
address |
Signal<Address | undefined> |
Destination address (shipping or branch) |
estimatedDelivery |
Signal<{ start: string; stop: string | null } | null> |
Calculated delivery date range |
Effects
branchChange: Automatically updatesBranchResourceparams when branch container changes
Injected Resources
BranchResource: Fetches branch details by IDSelectedCustomerShippingAddressResource: Fetches customer shipping address
Order Type Behavior
| Order Type | Icon | Display Mode | Data Source |
|---|---|---|---|
| Versand (Delivery) | isaDeliveryVersand |
Estimated delivery dates | Availability data |
| Abholung (Pickup) | isaDeliveryRuecklage2 |
Branch name + address | Destination.targetBranch |
| Rücklage (InStore) | isaDeliveryRuecklage1 |
Branch name + address | Destination.targetBranch |
| B2B-Versand | isaDeliveryVersand |
Customer name + shipping address | ShippingAddressResource |
| DIG-Versand | isaDeliveryVersand |
Customer name + shipping address | ShippingAddressResource |
Example
@Component({
template: `
<checkout-destination-info
[shoppingCartItem]="cartItem()"
[underline]="true"
/>
`,
})
export class ShippingInfoComponent {
cartItem = signal({
availability: {
estimatedDelivery: {
start: '2024-06-10T00:00:00+02:00',
stop: '2024-06-12T00:00:00+02:00',
},
},
destination: {
data: {
target: ShippingTarget.Branch,
targetBranch: {
id: 42,
data: {
name: 'Hauptfiliale München',
address: {
street: 'Marienplatz',
streetNumber: '1',
zipCode: '80331',
city: 'München',
},
},
},
},
},
features: {
orderType: 'Abholung',
},
});
}
StockInfoComponent
Displays real-time stock availability with asynchronous loading states.
Selector
<checkout-stock-info />
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
item |
StockInfoItem |
- | ✓ | Item with catalog availability information |
Outputs
None - presentation component only.
Computed Properties
| Name | Type | Description |
|---|---|---|
itemId |
Signal<number> |
Extracted item ID from input |
inStock |
Signal<number> |
Current stock quantity (0 if not loaded) |
loading |
Signal<boolean> |
Loading state from resource |
Public Properties
| Name | Type | Description |
|---|---|---|
stockInfoResource |
ResourceRef<StockInfo> |
Exposed resource reference for external access |
Exported As
<checkout-stock-info #stockInfo="stockInfo" />
Access resource methods:
@ViewChild('stockInfo', { read: StockInfoComponent })
stockInfoRef!: StockInfoComponent;
reloadStock() {
this.stockInfoRef.stockInfoResource.reload();
}
Resource Integration
Uses StockInfoResource from @isa/remission/data-access with automatic parameter updates:
readonly stockInfoResource = this.#stockInfoResource.resource(
computed(() => ({ itemId: this.itemId() }))
);
Loading State Handling
The component renders a skeleton loader during data fetching:
@if (loading()) {
<ui-skeleton-loader class="w-8 h-5 inline-block"></ui-skeleton-loader>
} @else {
{{ inStock() }}x
}
Example
@Component({
template: `
<checkout-stock-info
#stockInfo="stockInfo"
[item]="productItem()"
/>
<button (click)="stockInfo.stockInfoResource.reload()">
Refresh Stock
</button>
`,
})
export class ProductAvailabilityComponent {
productItem = signal<StockInfoItem>({
id: 123456,
catalogAvailability: {
ssc: '10',
sscText: 'Sofort lieferbar',
},
});
}
Type Definitions
ProductInfoItem
Discriminated union type supporting two product sources:
type ProductInfoItem =
| {
product: Pick<
CatProduct,
'ean' | 'name' | 'contributors' | 'format' |
'formatDetail' | 'manufacturer' | 'publicationDate'
>;
redemptionPoints?: number;
}
| {
product: Pick<
CheckoutProduct,
'ean' | 'name' | 'contributors' | 'format' |
'formatDetail' | 'manufacturer' | 'publicationDate'
>;
loyalty: Pick<Loyalty, 'value'>;
};
Catalog Product Variant:
- Uses
@isa/catalogue/data-accessProduct type - Optional
redemptionPointsfield - Typical use: Reward catalog browsing
Checkout Product Variant:
- Uses
@isa/checkout/data-accessProduct type - Required
loyaltyobject withvaluefield - Typical use: Shopping cart redemption items
Type Discrimination:
const item: ProductInfoItem = {...};
if ('redemptionPoints' in item) {
// Catalog product variant
const points = item.redemptionPoints ?? 0;
}
if ('loyalty' in item) {
// Checkout product variant
const points = item.loyalty.value ?? 0;
}
StockInfoItem
Simple type for stock availability display:
type StockInfoItem = {
id: Item['id']; // Product item ID
catalogAvailability: Pick<Item['catalogAvailability'], 'ssc' | 'sscText'>;
};
Fields:
id: Numeric item identifier for stock lookupcatalogAvailability.ssc: Stock status code (e.g., "10", "999")catalogAvailability.sscText: Human-readable status description
Usage Examples
Horizontal Product Info Layout
@Component({
template: `
<checkout-product-info-redemption
[item]="product()"
[orientation]="'horizontal'"
class="border-b pb-4"
/>
`,
imports: [ProductInfoRedemptionComponent],
})
export class HorizontalRewardComponent {
product = signal<ProductInfoItem>({
product: {
ean: '9783442488391',
name: 'Das Labyrinth des Fauns',
contributors: 'del Toro, Guillermo; Funke, Cornelia',
format: 'HC',
formatDetail: 'Hardcover (gebunden)',
manufacturer: 'Goldmann Verlag',
publicationDate: '2019-07-01',
},
redemptionPoints: 200,
});
}
Multiple Destination Types
@Component({
template: `
@for (item of cartItems(); track item.id) {
<checkout-destination-info
[shoppingCartItem]="item"
[underline]="true"
class="mb-4"
/>
}
`,
imports: [DestinationInfoComponent],
})
export class MultiDestinationComponent {
cartItems = signal([
{
id: 1,
availability: {
estimatedDelivery: {
start: '2024-06-15T00:00:00+02:00',
stop: null,
},
},
destination: { data: { target: ShippingTarget.Delivery } },
features: { orderType: 'Versand' },
},
{
id: 2,
destination: {
data: {
target: ShippingTarget.Branch,
targetBranch: {
id: 42,
data: {
name: 'Filiale Hamburg',
address: {
street: 'Mönckebergstraße',
streetNumber: '10',
zipCode: '20095',
city: 'Hamburg',
},
},
},
},
},
features: { orderType: 'Abholung' },
},
]);
}
Stock Info with Manual Reload
@Component({
template: `
<checkout-stock-info
#stockRef="stockInfo"
[item]="productItem()"
/>
<button
(click)="refreshStock()"
[disabled]="stockRef.loading()"
class="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
>
{{ stockRef.loading() ? 'Loading...' : 'Refresh Stock' }}
</button>
`,
imports: [StockInfoComponent],
})
export class StockRefreshComponent {
@ViewChild('stockRef', { read: StockInfoComponent })
stockInfoRef!: StockInfoComponent;
productItem = signal<StockInfoItem>({
id: 789012,
catalogAvailability: {
ssc: '20',
sscText: 'Lieferbar in 5-7 Werktagen',
},
});
refreshStock() {
this.stockInfoRef.stockInfoResource.reload();
}
}
Combined Product Display
@Component({
template: `
<div class="grid grid-cols-[2fr,1fr] gap-6 p-4 border rounded">
<!-- Left: Product Info -->
<checkout-product-info-redemption
[item]="productData()"
[orientation]="'vertical'"
/>
<!-- Right: Stock & Destination -->
<div class="flex flex-col gap-4">
<checkout-stock-info [item]="stockData()" />
<checkout-destination-info
[shoppingCartItem]="cartData()"
[underline]="false"
/>
</div>
</div>
`,
imports: [
ProductInfoRedemptionComponent,
StockInfoComponent,
DestinationInfoComponent,
],
})
export class ProductCardComponent {
productData = signal<ProductInfoItem>({
product: {
ean: '9783453441378',
name: 'Der Schwarm',
contributors: 'Schätzing, Frank',
format: 'TB',
formatDetail: 'Taschenbuch (Kartoniert)',
manufacturer: 'Heyne Verlag',
publicationDate: '2005-08-01',
},
redemptionPoints: 300,
});
stockData = signal<StockInfoItem>({
id: 456789,
catalogAvailability: {
ssc: '10',
sscText: 'Sofort lieferbar',
},
});
cartData = signal({
availability: {
estimatedShippingDate: '2024-06-08T00:00:00+02:00',
},
destination: { data: { target: ShippingTarget.Delivery } },
features: { orderType: 'Versand' },
});
}
Loyalty-Based Redemption
@Component({
template: `
<checkout-product-info-redemption
[item]="loyaltyProduct()"
[orientation]="'horizontal'"
/>
`,
imports: [ProductInfoRedemptionComponent],
})
export class LoyaltyRedemptionComponent {
loyaltyProduct = signal<ProductInfoItem>({
product: {
ean: '9783423140942',
name: 'Der Vorleser',
contributors: 'Schlink, Bernhard',
format: 'TB',
formatDetail: 'Taschenbuch (Kartoniert)',
manufacturer: 'dtv Verlag',
publicationDate: '1997-09-01',
},
loyalty: {
value: 250, // Loyalty points instead of redemptionPoints
},
});
}
Conditional Rendering Based on Order Type
@Component({
template: `
<checkout-destination-info
[shoppingCartItem]="cartItem()"
[underline]="shouldUnderline()"
/>
@if (isDelivery()) {
<p class="mt-2 text-sm text-neutral-600">
Standard shipping applies (3-5 business days)
</p>
}
@if (isPickup() || isInStore()) {
<p class="mt-2 text-sm text-neutral-600">
Please bring a valid ID when picking up
</p>
}
`,
imports: [DestinationInfoComponent],
})
export class ConditionalDestinationComponent {
cartItem = signal({
availability: {
estimatedDelivery: {
start: '2024-06-20T00:00:00+02:00',
stop: '2024-06-22T00:00:00+02:00',
},
},
destination: { data: { target: ShippingTarget.Delivery } },
features: { orderType: 'Versand' },
});
orderType = computed(() =>
getOrderTypeFeature(this.cartItem().features)
);
isDelivery = computed(() => this.orderType() === OrderType.Delivery);
isPickup = computed(() => this.orderType() === OrderType.Pickup);
isInStore = computed(() => this.orderType() === OrderType.InStore);
shouldUnderline = computed(() => !this.isDelivery());
}
Styling and Customization
Tailwind CSS Integration
All components use ISA design system utilities with Tailwind CSS:
Typography Classes
isa-text-body-2-bold: Bold body text (14px)isa-text-body-2-regular: Regular body text (14px)isa-text-subtitle-1-regular: Larger subtitle text (16px)
Color Classes
text-neutral-900: Primary text colortext-neutral-600: Secondary text colortext-isa-secondary-900: Secondary accent color
Layout Classes
grid,flex: Flexbox and grid layoutsgap-2,gap-4,gap-6: Spacing utilitiesitems-center,items-start: Alignment utilities
Component-Specific Styles
ProductInfoRedemptionComponent
Host Styles:
:host {
@apply grid gap-6 text-neutral-900;
}
:host.horizontal {
@apply grid-cols-[1fr,minmax(0,25rem)];
}
:host.vertical {
@apply grid-flow-row;
}
Custom Classes:
.checkout-product-info-redemption__image {
@apply w-14; /* 56px fixed width */
}
shared-product-format span {
@apply isa-text-body-2-bold text-isa-secondary-900;
}
Layout Variations:
- Horizontal: Two-column grid with 1fr left, max-width 25rem right
- Vertical: Single-column flow with left margin on metadata (ml-20)
DestinationInfoComponent
Host Styles:
:host {
@apply flex flex-col items-start gap-2 flex-grow;
}
Custom Classes:
.address-container {
@apply line-clamp-2 break-words text-ellipsis;
}
.underline {
/* Applied dynamically via [class.underline] binding */
}
Icon Sizing:
- All icons:
1.5rem(24px) - Color:
text-neutral-900
StockInfoComponent
Host Styles:
:host {
@apply grid grid-flow-row gap-2;
}
Grid Layout:
.grid.grid-cols-\[auto\,1fr\] {
/* Icon column: auto-sized */
/* Content column: fills remaining space */
}
Customization Examples
Custom Product Image Size
@Component({
template: `
<checkout-product-info-redemption
[item]="product()"
class="custom-image-size"
/>
`,
styles: [`
::ng-deep .custom-image-size .checkout-product-info-redemption__image {
@apply w-20 h-28; /* Custom dimensions */
}
`],
})
export class CustomImageComponent { }
Custom Destination Styling
@Component({
template: `
<checkout-destination-info
[shoppingCartItem]="item()"
class="custom-destination"
/>
`,
styles: [`
.custom-destination {
@apply bg-neutral-100 p-4 rounded-lg;
}
.custom-destination .address-container {
@apply line-clamp-3; /* Show 3 lines instead of 2 */
}
`],
})
export class CustomDestinationComponent { }
Custom Stock Info Colors
@Component({
template: `
<checkout-stock-info
[item]="stockItem()"
class="stock-highlight"
/>
`,
styles: [`
.stock-highlight {
@apply border-l-4 border-green-500 pl-4;
}
::ng-deep .stock-highlight .isa-text-body-2-bold {
@apply text-green-700;
}
`],
})
export class CustomStockComponent { }
Architecture Notes
Design Patterns
1. Presentation Components
All three components follow the Presentation Component pattern:
- No business logic or state management
- Pure transformation of input data to visual output
- Signal-based inputs for optimal change detection
- OnPush change detection for performance
2. Resource Integration
DestinationInfoComponent and StockInfoComponent demonstrate the Resource Provider pattern:
- Self-contained resource management
- Automatic parameter updates via computed signals
- Loading state handling
- Error boundary support
3. Type Safety
Components use Discriminated Unions for flexible input types:
ProductInfoItemsupports both catalog and checkout products- Type narrowing via
'redemptionPoints' in itemchecks - Prevents runtime type errors
Component Responsibilities
ProductInfoRedemptionComponent
Responsibility: Display product metadata and reward points Dependencies: Product image/router directives, product format component State: Stateless (pure transformation) Integration Points: Product detail routing, image service
DestinationInfoComponent
Responsibility: Display destination with order-type-specific formatting Dependencies: Branch and shipping address resources State: Resource-managed (branch, shipping address) Integration Points: CRM shipping addresses, branch management
StockInfoComponent
Responsibility: Display real-time stock availability Dependencies: Stock info resource, skeleton loader State: Resource-managed (stock data) Integration Points: Inventory API, remission stock service
Known Architectural Considerations
1. Resource Provider Scope (Low Priority)
Current State:
DestinationInfoComponentandStockInfoComponentprovide resources at component level- Each component instance creates its own resource instances
Impact:
- Multiple instances fetch data independently
- No resource sharing between sibling components
Consideration:
- For list views with many instances, consider parent-level resource provision
- Example: Product list with stock info for each item
Optimization Strategy:
@Component({
template: `
@for (item of items(); track item.id) {
<checkout-stock-info [item]="item" />
}
`,
providers: [StockInfoResource], // Shared resource for all children
})
export class ProductListComponent { }
2. Destination Info Complexity (Medium Priority)
Current State:
- Component handles 5 different order types
- Complex conditional logic for address vs. delivery date display
- Multiple resource dependencies (branch, shipping address)
Consideration:
- Consider decomposition into smaller components:
DestinationDeliveryComponent(Versand, DIG-Versand, B2B-Versand)DestinationBranchComponent(Abholung, Rücklage)
- Benefits: Simpler logic, fewer dependencies per component
Current Justification:
- Single component ensures consistent icon/header rendering
- Shared logic reduces code duplication
- Complexity is well-encapsulated
3. Product Info Type Union (Low Priority)
Current State:
- Discriminated union supports both catalog and checkout products
- Runtime type checking required (
'redemptionPoints' in item)
Alternative:
- Two separate components:
ProductInfoCatalog,ProductInfoCheckout - Eliminates runtime type checks
- More explicit component selection
Current Justification:
- Single component provides consistent rendering
- Type discrimination is lightweight
- Simplifies template usage
Performance Considerations
Change Detection Optimization
All components use OnPush change detection:
- Updates only when signal inputs change
- No unnecessary re-renders
- Optimal for large lists
Resource Request Deduplication
Resource APIs automatically deduplicate concurrent requests:
// Multiple components requesting same stock data
<checkout-stock-info [item]="{ id: 123, ... }" />
<checkout-stock-info [item]="{ id: 123, ... }" />
// Only one API request is made
Skeleton Loading States
StockInfoComponent shows skeleton loaders during data fetching:
- Prevents layout shift
- Better perceived performance
- User feedback during async operations
Future Enhancements
- Virtualization Support: Optimize for large product lists with virtual scrolling
- Caching Strategy: Implement resource-level caching for frequently accessed data
- Offline Support: Handle network failures gracefully with cached data
- Accessibility: Add ARIA labels and screen reader support
- Analytics Integration: Track user interactions with product info components
Testing
The library uses Vitest with Angular Testing Utilities for testing.
Running Tests
# Run tests for this library
npx nx test checkout-shared-product-info --skip-nx-cache
# Run tests with coverage
npx nx test checkout-shared-product-info --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test checkout-shared-product-info --watch
Test Structure
The library includes unit tests covering:
- Component creation - Validates successful component instantiation
- Input bindings - Tests signal input reactivity
- Computed properties - Validates derived signal calculations
- Resource integration - Tests resource parameter updates
- Loading states - Validates skeleton loader display
- E2E attributes - Ensures
data-whatattributes are present
Example Test: StockInfoComponent
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { StockInfoComponent } from './stock-info.component';
import { RemissionStockService } from '@isa/remission/data-access';
describe('StockInfoComponent', () => {
let component: StockInfoComponent;
let fixture: ComponentFixture<StockInfoComponent>;
let mockStockService: any;
beforeEach(() => {
mockStockService = {
fetchStockInfos: () => Promise.resolve([]),
};
TestBed.configureTestingModule({
imports: [StockInfoComponent],
providers: [
{ provide: RemissionStockService, useValue: mockStockService },
],
});
fixture = TestBed.createComponent(StockInfoComponent);
component = fixture.componentInstance;
fixture.componentRef.setInput('item', {
id: 123,
catalogAvailability: { ssc: '10', sscText: 'Available' },
});
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have item input', () => {
expect(component.item()).toEqual({
id: 123,
catalogAvailability: { ssc: '10', sscText: 'Available' },
});
});
it('should initialize with zero inStock when no data loaded', () => {
expect(component.inStock()).toBe(0);
});
it('should have stockInfoResource ResourceRef defined', () => {
expect(component.stockInfoResource).toBeDefined();
expect(component.stockInfoResource.value).toBeDefined();
expect(component.stockInfoResource.isLoading).toBeDefined();
expect(component.stockInfoResource.reload).toBeDefined();
});
});
Testing Best Practices
1. Signal Input Testing
Always use fixture.componentRef.setInput() for signal inputs:
// Correct - for signal inputs
fixture.componentRef.setInput('item', mockItem);
fixture.detectChanges();
// Incorrect - for signal inputs (won't work)
component.item = signal(mockItem);
2. Resource Mocking
Mock resource dependencies at the service level:
const mockStockService = {
fetchStockInfos: vi.fn().mockResolvedValue([
{ itemId: 123, inStock: 5 }
]),
};
TestBed.configureTestingModule({
providers: [
{ provide: RemissionStockService, useValue: mockStockService },
],
});
3. Computed Signal Testing
Test computed signals after input changes:
it('should calculate points from redemptionPoints', () => {
fixture.componentRef.setInput('item', {
product: { /* ... */ },
redemptionPoints: 150,
});
fixture.detectChanges();
expect(component.points()).toBe(150);
});
it('should calculate points from loyalty', () => {
fixture.componentRef.setInput('item', {
product: { /* ... */ },
loyalty: { value: 200 },
});
fixture.detectChanges();
expect(component.points()).toBe(200);
});
4. E2E Attribute Verification
Verify E2E attributes in template:
it('should have data-what attribute on product image', () => {
fixture.detectChanges();
const image = fixture.nativeElement.querySelector('img');
expect(image.getAttribute('data-what')).toBe('product-image');
});
Storybook Integration
Each component has comprehensive Storybook stories for visual testing:
Product Info Stories
// apps/isa-app/stories/checkout/shared/product-info/product-info-redemption.stories.ts
export const Primary: Story = {
args: {
item: {
product: {
ean: '9783498007706',
name: 'Die Assistentin',
contributors: 'Wahl, Caroline',
// ...
},
redemptionPoints: 100,
},
orientation: 'vertical',
},
};
Running Storybook
npm run storybook
# Navigate to: checkout/shared/product-info/*
CI/CD Testing
The library is included in CI test runs:
# CI command (used in pipelines)
npm run ci
# Or: npx nx run-many -t test -c ci
Dependencies
Required Libraries
Angular Core
@angular/core- Angular framework@angular/common- Common Angular directives (DatePipe)@angular/cdk/coercion- Coercion utilities (boolean inputs)
Domain Libraries
@isa/catalogue/data-access- Catalog product types@isa/checkout/data-access- Checkout product types, order types, shopping cart@isa/crm/data-access- Customer shipping address resource@isa/remission/data-access- Stock service and branch resource
Shared Components
@isa/shared/product-image- Product image directive@isa/shared/product-router-link- Product routing directive@isa/shared/product-format- Product format display component@isa/shared/address- Inline address component
UI Components
@isa/ui/skeleton-loader- Loading skeleton component@ng-icons/core- Icon component@isa/icons- ISA icon set (isaFiliale, isaDeliveryVersand, etc.)
Generated API
@generated/swagger/inventory-api- Stock info DTO types
Development Dependencies
vitest- Testing framework@angular/platform-browser-dynamic- Testing utilities@storybook/angular- Storybook integration
Path Alias
Import from: @isa/checkout/shared/product-info
Peer Dependencies
These must be provided by the consuming application:
- Angular 20.1.2+
- RxJS 7.5+
- Tailwind CSS with ISA design system configuration
License
Internal ISA Frontend library - not for external distribution.