Files
Lorenz Hilpert 7950994d66 Merged PR 2057: feat(checkout): add branch selection to reward catalog
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
2025-11-27 10:38:52 +00:00
..

@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

  • 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-what attributes 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 to orientation() for CSS styling

Template Features

  • Product Image: Clickable product image with sharedProductRouterLink directive
  • 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-format component
  • 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 updates BranchResource params when branch container changes

Injected Resources

  • BranchResource: Fetches branch details by ID
  • SelectedCustomerShippingAddressResource: 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-access Product type
  • Optional redemptionPoints field
  • Typical use: Reward catalog browsing

Checkout Product Variant:

  • Uses @isa/checkout/data-access Product type
  • Required loyalty object with value field
  • 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 lookup
  • catalogAvailability.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 color
  • text-neutral-600: Secondary text color
  • text-isa-secondary-900: Secondary accent color

Layout Classes

  • grid, flex: Flexbox and grid layouts
  • gap-2, gap-4, gap-6: Spacing utilities
  • items-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:

  • ProductInfoItem supports both catalog and checkout products
  • Type narrowing via 'redemptionPoints' in item checks
  • 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:

  • DestinationInfoComponent and StockInfoComponent provide 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

  1. Virtualization Support: Optimize for large product lists with virtual scrolling
  2. Caching Strategy: Implement resource-level caching for frequently accessed data
  3. Offline Support: Handle network failures gracefully with cached data
  4. Accessibility: Add ARIA labels and screen reader support
  5. 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-what attributes 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.