- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
31 KiB
@isa/oms/shared/product-info
A reusable Angular component library for displaying product information in a standardized, visually consistent format across Order Management System (OMS) workflows.
Overview
The Product Info library provides the ReturnProductInfoComponent, a presentation component that displays product details including product image, contributors, name, and format information. It's specifically designed for use in return process workflows but can be adapted for any OMS context where consistent product information display is needed.
The component integrates seamlessly with the ISA design system, utilizing Tailwind CSS utilities and the ISA typography system for consistent visual presentation.
Table of Contents
- Features
- Quick Start
- Component API Reference
- Usage Examples
- Styling and Customization
- Architecture Notes
- Dependencies
- Testing
- Best Practices
Features
- Standalone Component Architecture - Modern Angular standalone component with explicit imports
- OnPush Change Detection - Optimized performance with ChangeDetectionStrategy.OnPush
- Signal-based Inputs - Uses Angular signals for reactive property updates
- Product Image Integration - Automatic product image loading via EAN with
ProductImageDirective - Product Navigation - Built-in navigation to product details via
ProductRouterLinkDirective - Format Icons - Visual format indicators using icon system (Hardcover, Paperback, CD, etc.)
- E2E Testing Attributes - Comprehensive
data-whatanddata-whichattributes for automated testing - ISA Design System Integration - Consistent typography and spacing using ISA Tailwind utilities
- BEM-like SCSS Structure - Maintainable component-scoped styling with clear naming
- Type Safety - Full TypeScript integration with Zod-validated Product schema
Quick Start
1. Import the Component
import { Component } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { Product } from '@isa/oms/data-access';
@Component({
selector: 'app-return-summary',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
<oms-shared-return-product-info
[product]="product"
></oms-shared-return-product-info>
`
})
export class ReturnSummaryComponent {
product: Product = {
ean: '9783161484100',
name: 'The Great Gatsby',
contributors: 'F. Scott Fitzgerald',
format: 'HC',
formatDetail: 'Hardcover',
// ... other product properties
};
}
2. Basic Template Usage
<!-- Minimal usage -->
<oms-shared-return-product-info
[product]="product"
></oms-shared-return-product-info>
<!-- With E2E attributes -->
<oms-shared-return-product-info
[product]="product"
data-what="component"
data-which="return-product-info"
></oms-shared-return-product-info>
3. Integration with Signals
import { Component, signal } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { Product } from '@isa/oms/data-access';
@Component({
selector: 'app-product-display',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
<oms-shared-return-product-info
[product]="currentProduct()"
></oms-shared-return-product-info>
`
})
export class ProductDisplayComponent {
currentProduct = signal<Product>({
ean: '9783161484100',
name: 'Angular Essentials',
contributors: 'John Doe',
format: 'TB',
formatDetail: 'Paperback',
// ... other properties
});
updateProduct(newProduct: Product): void {
this.currentProduct.set(newProduct);
}
}
Component API Reference
ReturnProductInfoComponent
A presentation component that displays product information in a standardized layout with image, contributors, name, and format.
Selector
'oms-shared-return-product-info'
Inputs
| Input | Type | Required | Description |
|---|---|---|---|
product |
Product |
Yes | Product object containing EAN, name, contributors, format, and other details |
Product Model Structure
The component expects a Product object with the following structure:
interface Product {
// Product Identification
ean?: string; // European Article Number (barcode)
catalogProductNumber?: string; // Internal catalog number
supplierProductNumber?: string; // Supplier's product number
// Product Information
name?: string; // Product name/title
additionalName?: string; // Subtitle or additional name
contributors?: string; // Authors, artists, or contributors
manufacturer?: string; // Manufacturer/publisher name
// Format and Physical Details
format?: string; // Format code (HC, TB, AU, etc.)
formatDetail?: string; // Human-readable format (Hardcover, Paperback, etc.)
size?: any; // Physical size information
weight?: any; // Weight information
volume?: string; // Volume number or identifier
// Classification
productGroup?: string; // Product group classification
productGroupDetails?: string; // Detailed product group info
edition?: string; // Edition information
locale?: string; // Language/locale code
serial?: string; // Serial number
publicationDate?: string; // Publication date (ISO format)
// TouchBase fields (from TouchBaseSchema)
// Additional timestamp and metadata fields
}
Template Structure
The component renders the following structure:
<div class="return-product-info" data-what="product-info" data-which="return-product">
<!-- Product Image with Navigation -->
<img
sharedProductRouterLink
sharedProductImage
[ean]="product().ean"
[alt]="product().name"
class="return-product-info__image w-14"
data-what="product-image"
/>
<!-- Product Details -->
<div class="return-product-info__content">
<!-- Contributors (Bold) -->
<div
class="return-product-info__contributors isa-text-body-2-bold text-isa-neutral-900"
data-what="product-contributors"
>
{{ product().contributors }}
</div>
<!-- Product Name (Regular) -->
<div
class="return-product-info__name isa-text-body-2-regular text-isa-neutral-900"
data-what="product-name"
>
{{ product().name }}
</div>
<!-- Format Icon and Detail -->
<div class="return-product-info__format" data-what="product-format">
<ng-icon [name]="product().format | lowercase"></ng-icon>
<span class="text-isa-secondary-900">{{ product().formatDetail }}</span>
</div>
</div>
</div>
E2E Testing Attributes
The component includes comprehensive testing attributes:
| Element | data-what |
data-which |
Description |
|---|---|---|---|
| Root container | product-info |
return-product |
Main component container |
| Product image | product-image |
- | Product thumbnail image |
| Contributors | product-contributors |
- | Author/artist/contributor names |
| Product name | product-name |
- | Product title/name |
| Format section | product-format |
- | Format icon and detail container |
Change Detection
- Strategy:
ChangeDetectionStrategy.OnPush - Benefit: Component only re-renders when input references change
- Usage: Provides optimal performance in lists and grids
Usage Examples
Basic Product Display in Return Workflow
import { Component, inject } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { ReturnProcessStore } from '@isa/oms/data-access';
@Component({
selector: 'app-return-summary-item',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
@let product = returnProcess().receiptItem.product;
<oms-shared-return-product-info
[product]="product"
data-what="component"
data-which="return-product-info"
></oms-shared-return-product-info>
`
})
export class ReturnSummaryItemComponent {
#returnProcessStore = inject(ReturnProcessStore);
returnProcess = this.#returnProcessStore.returnProcess;
}
Product List Display
import { Component, signal } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { Product } from '@isa/oms/data-access';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
<div class="product-grid grid gap-4">
@for (product of products(); track product.ean) {
<div class="product-card border rounded-lg p-4">
<oms-shared-return-product-info
[product]="product"
[attr.data-product-ean]="product.ean"
></oms-shared-return-product-info>
</div>
}
</div>
`
})
export class ProductListComponent {
products = signal<Product[]>([
{
ean: '9783161484100',
name: 'The Great Gatsby',
contributors: 'F. Scott Fitzgerald',
format: 'HC',
formatDetail: 'Hardcover',
catalogProductNumber: 'CAT-001',
},
{
ean: '9780142437230',
name: '1984',
contributors: 'George Orwell',
format: 'TB',
formatDetail: 'Paperback',
catalogProductNumber: 'CAT-002',
}
]);
}
Async Product Loading
import { Component, signal, OnInit, inject } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { Product } from '@isa/oms/data-access';
import { ProductService } from './product.service';
@Component({
selector: 'app-product-detail',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
@if (product(); as prod) {
<div class="product-detail-card">
<oms-shared-return-product-info
[product]="prod"
></oms-shared-return-product-info>
<!-- Additional product details -->
<div class="mt-4">
<p>EAN: {{ prod.ean }}</p>
<p>Publisher: {{ prod.manufacturer }}</p>
</div>
</div>
} @else {
<div class="loading">Loading product...</div>
}
`
})
export class ProductDetailComponent implements OnInit {
#productService = inject(ProductService);
product = signal<Product | null>(null);
async ngOnInit(): Promise<void> {
const productData = await this.#productService.getProduct('9783161484100');
this.product.set(productData);
}
}
Integration with Task List
import { Component, input } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { Product } from '@isa/oms/data-access';
interface TaskItem {
id: string;
product: Product;
quantity: number;
status: 'pending' | 'completed';
}
@Component({
selector: 'app-task-list-item',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
<div class="task-item flex items-start gap-4 p-4 border-b">
<!-- Product Info -->
<oms-shared-return-product-info
[product]="task().product"
></oms-shared-return-product-info>
<!-- Task Metadata -->
<div class="task-meta">
<span>Quantity: {{ task().quantity }}</span>
<span>Status: {{ task().status }}</span>
</div>
</div>
`
})
export class TaskListItemComponent {
task = input.required<TaskItem>();
}
Custom E2E Attributes for Complex Workflows
@Component({
selector: 'app-return-process-item',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
<div
class="return-item"
[attr.data-return-id]="returnId()"
[attr.data-step]="currentStep()"
>
<oms-shared-return-product-info
[product]="product()"
data-what="component"
data-which="return-product-info"
[attr.data-product-ean]="product().ean"
[attr.data-return-status]="returnStatus()"
></oms-shared-return-product-info>
</div>
`
})
export class ReturnProcessItemComponent {
returnId = input.required<string>();
product = input.required<Product>();
currentStep = input<number>(1);
returnStatus = input<string>('pending');
}
Conditional Product Display
import { Component, signal, computed } from '@angular/core';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { Product } from '@isa/oms/data-access';
@Component({
selector: 'app-conditional-product',
standalone: true,
imports: [ReturnProductInfoComponent],
template: `
@if (isProductAvailable()) {
<oms-shared-return-product-info
[product]="product()!"
></oms-shared-return-product-info>
} @else {
<div class="product-unavailable">
<p>Product information unavailable</p>
</div>
}
`
})
export class ConditionalProductComponent {
product = signal<Product | null>(null);
isProductAvailable = computed(() => {
const prod = this.product();
return prod !== null && prod.ean !== undefined;
});
}
Styling and Customization
Component Styling Structure
The component uses a BEM-like SCSS structure with the following classes:
.return-product-info {
display: flex;
align-items: flex-start;
gap: 1.5rem;
}
.return-product-info__image {
border-radius: 0.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.return-product-info__content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.return-product-info__format {
display: flex;
align-items: center;
gap: 0.5rem;
}
Layout Characteristics
- Flexbox Layout: Horizontal layout with image on left, content on right
- Alignment:
flex-startfor top alignment of image and content - Gap:
1.5rem(24px) between image and content - Image: Fixed width of
w-14(56px) with rounded corners and subtle shadow - Content Stack: Vertical layout with
0.25rem(4px) gap between elements
ISA Design System Integration
The component uses ISA-specific Tailwind utilities:
| Element | Typography Class | Color Class | Description |
|---|---|---|---|
| Contributors | isa-text-body-2-bold |
text-isa-neutral-900 |
Bold, dark neutral color |
| Product Name | isa-text-body-2-regular |
text-isa-neutral-900 |
Regular weight, dark neutral |
| Format Detail | - | text-isa-secondary-900 |
Secondary color for format text |
| Image | w-14 |
- | 56px fixed width thumbnail |
Customizing via CSS Custom Properties
While the component uses scoped styles, you can customize the appearance by wrapping it:
<div class="custom-product-info">
<oms-shared-return-product-info
[product]="product"
></oms-shared-return-product-info>
</div>
.custom-product-info {
// Adjust the overall container
::ng-deep .return-product-info {
gap: 2rem; // Increase spacing
padding: 1rem; // Add padding
background-color: var(--isa-neutral-50); // Add background
border-radius: 0.5rem;
}
// Adjust the image size
::ng-deep .return-product-info__image {
width: 4rem; // 64px instead of 56px
height: 4rem;
}
}
Note: Use ::ng-deep sparingly and only when necessary, as it breaks view encapsulation.
Format Icons
The component supports the following format icons from ProductFormatIconGroup:
| Format Code | Icon | Description |
|---|---|---|
tb |
isaArtikelTaschenbuch |
Paperback/Taschenbuch |
hc |
isaArtikelKartoniert |
Hardcover/Kartoniert |
au |
isaArtikelCd |
Audio CD |
ka |
isaArtikelSonstige |
Card/Other |
sw |
isaArtikelCd |
Software/CD |
nb |
isaArtikelGame |
Game/Notebook |
The format code from product.format is automatically lowercased and mapped to the corresponding icon.
Responsive Design Considerations
The component uses fixed-width layout suitable for most contexts. For responsive behavior:
<!-- Stack on mobile, horizontal on desktop -->
<div class="product-wrapper">
<oms-shared-return-product-info
[product]="product"
></oms-shared-return-product-info>
</div>
.product-wrapper {
// Override flex direction on mobile
@media (max-width: 640px) {
::ng-deep .return-product-info {
flex-direction: column;
align-items: center;
}
}
}
Architecture Notes
Component Design Pattern
The component follows the Presentation Component pattern:
- No Business Logic: Pure presentation with no service injection
- Signal Input: Reactive data binding via Angular signals
- OnPush Detection: Optimal performance with immutable inputs
- Standalone: No module dependencies, explicit imports only
Directive Integration
The component integrates two key directives:
1. ProductImageDirective (sharedProductImage)
- Purpose: Automatically loads product images from EAN
- Service: Uses
ProductImageServiceto construct image URLs - Inputs:
ean(required): Product EAN for image lookupimageWidth(optional): Image width, defaults to 150pximageHeight(optional): Image height, defaults to 150px
- Behavior: Computes image URL and binds to
[src]attribute
2. ProductRouterLinkDirective (sharedProductRouterLink)
- Purpose: Makes product images clickable and navigable
- Service: Uses injected
PRODUCT_ROUTER_LINK_BUILDERfor URL construction - Inputs:
ean(required): Product EAN for routing
- Behavior: Extends
RouterLinkand builds URL asynchronously via effect - Visual: Adds
cursor-pointerclass for clickable appearance
Icon System Integration
The component uses @ng-icons/core with the ISA icon library:
providers: [provideIcons({ ...ProductFormatIconGroup })]
This provides format-specific icons that are:
- Automatically selected based on lowercase format code
- Rendered inline with format detail text
- Scalable and themeable
Change Detection Strategy
ChangeDetectionStrategy.OnPush ensures:
- Component only re-renders when
productinput reference changes - Optimal performance in lists with many product items
- Requires immutable data patterns (use
.set()not.mutate()on signals)
E2E Testing Architecture
The component follows ISA's E2E testing standards:
data-what="product-info" <!-- Element type -->
data-which="return-product" <!-- Element purpose/context -->
This enables QA automation to:
- Locate elements independent of styling changes
- Verify product information display
- Test product navigation behavior
- Validate format icon rendering
Dependency Graph
ReturnProductInfoComponent
├─→ @isa/oms/data-access (Product type)
├─→ @isa/shared/product-image (ProductImageDirective)
├─→ @isa/shared/product-router-link (ProductRouterLinkDirective)
├─→ @isa/icons (ProductFormatIconGroup)
├─→ @ng-icons/core (NgIcon, provideIcons)
└─→ @angular/common (LowerCasePipe)
Known Architectural Considerations
1. Naming Convention (Low Priority)
Current State:
- Component name includes "Return" prefix (
ReturnProductInfoComponent) - Selector uses
oms-shared-return-product-info - Implies exclusive use in return workflows
Consideration:
- Component is generic and could be used in any product display context
- Consider renaming to
ProductInfoComponentfor broader reusability - Would require migration of existing usage sites
Impact: Low - component works well as-is, naming is semantic for current use
2. Product Model Optional Fields
Current State:
- All
Productfields are optional (string | undefined) - Component assumes
ean,name,contributors,format, andformatDetailexist - No null/undefined handling in template
Consideration:
- Component may render empty strings if required fields are undefined
- Consider adding default values or conditional rendering
- Could add input validation or required field checks
Impact: Medium - potential for incomplete UI if product data is partial
Recommended Enhancement:
@Component({
template: `
@if (hasRequiredFields()) {
<!-- Current template -->
} @else {
<div class="product-info-error">Incomplete product data</div>
}
`
})
export class ReturnProductInfoComponent {
product = input.required<Product>();
hasRequiredFields = computed(() => {
const prod = this.product();
return !!(prod.ean && prod.name && prod.contributors);
});
}
3. Image Loading States
Current State:
- No loading state or error handling for product images
ProductImageDirectivehandles URL construction- Missing image will show broken image icon
Consideration:
- Add loading skeleton or placeholder
- Handle image load errors gracefully
- Consider lazy loading for lists
Impact: Medium - affects user experience with slow networks or missing images
4. Format Icon Fallback
Current State:
- Format code is lowercased and mapped to icon name
- If format code doesn't match icon group, icon won't render
- No fallback icon for unknown formats
Consideration:
- Add default icon for unknown format codes
- Handle undefined/null format gracefully
- Log warning when format is unmapped
Impact: Low - most products have standard format codes
Dependencies
Required Angular Packages
@angular/core^20.1.2 - Angular framework (signals, components, change detection)@angular/common- Common Angular utilities (LowerCasePipe)
Required ISA Libraries
@isa/oms/data-access- Product type definition and Zod schema@isa/shared/product-image- ProductImageDirective for image loading@isa/shared/product-router-link- ProductRouterLinkDirective for navigation@isa/icons- ProductFormatIconGroup icon set
Required Third-Party Packages
@ng-icons/core- Icon rendering systemzod- Runtime schema validation (Product type)
Development Dependencies
@ngneat/spectator- Testing utilities (current tests)jest- Testing frameworkng-mocks- Directive mocking for tests
Path Alias
Import from: @isa/oms/shared/product-info
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
Testing
The library uses Jest with Spectator for testing.
Running Tests
# Run tests for this library
npx nx test oms-shared-product-info --skip-nx-cache
# Run tests with coverage
npx nx test oms-shared-product-info --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test oms-shared-product-info --watch
Test Structure
The component includes comprehensive unit tests covering:
- Component Creation - Validates component instantiation
- Product Display - Tests rendering of name and contributors
- Image Directive Binding - Verifies EAN is passed to ProductImageDirective
- Alt Text - Validates image alt attribute matches product name
- Format Display - Tests format icon and formatDetail rendering
- E2E Attributes - Validates all testing attributes are present
Example Test
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ReturnProductInfoComponent } from './return-product-info.component';
import { Product } from '@isa/oms/data-access';
const MOCK_PRODUCT: Product = {
catalogProductNumber: '123456',
ean: '1234567890123',
manufacturer: 'Example Publisher',
name: 'Example Product Name',
volume: '2',
contributors: 'John Doe, Jane Smith',
format: 'HC',
formatDetail: 'Hardcover',
};
describe('ReturnProductInfoComponent', () => {
let spectator: Spectator<ReturnProductInfoComponent>;
const createComponent = createComponentFactory({
component: ReturnProductInfoComponent,
// Mock directives to avoid external dependencies
detectChanges: false,
});
beforeEach(() => {
spectator = createComponent({
props: { product: MOCK_PRODUCT }
});
spectator.detectChanges();
});
it('should display product name and contributors', () => {
const contributorsEl = spectator.query('[data-what="product-contributors"]');
const nameEl = spectator.query('[data-what="product-name"]');
expect(contributorsEl).toHaveText(MOCK_PRODUCT.contributors);
expect(nameEl).toHaveText(MOCK_PRODUCT.name);
});
it('should pass correct EAN to product image directive', () => {
const imgElement = spectator.query('img');
const productImageDirective = spectator.query(ProductImageDirective);
expect(productImageDirective?.ean).toBe(MOCK_PRODUCT.ean);
expect(imgElement).toHaveAttribute('alt', MOCK_PRODUCT.name);
});
});
Test Coverage
Current test coverage includes:
- ✅ Component instantiation
- ✅ Product name display
- ✅ Contributors display
- ✅ EAN binding to image directive
- ✅ Alt text for accessibility
- ✅ Format icon rendering
- ✅ Format detail text display
- ✅ E2E attribute presence
Testing Best Practices
- Mock Directives: Use
MockDirective()fromng-mocksfor ProductImageDirective and ProductRouterLinkDirective - Mock Product Data: Create comprehensive mock products with all required fields
- E2E Selectors: Use
data-whatattributes for element selection in tests - Signal Testing: Test computed signals and reactive updates
- Accessibility: Verify alt text and semantic HTML structure
Migration to Vitest (Planned)
This library is scheduled for migration to Vitest + Angular Testing Utilities:
// Future test structure
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from 'vitest';
import { ReturnProductInfoComponent } from './return-product-info.component';
describe('ReturnProductInfoComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ReturnProductInfoComponent]
});
});
it('should create', () => {
const fixture = TestBed.createComponent(ReturnProductInfoComponent);
expect(fixture.componentInstance).toBeTruthy();
});
});
Best Practices
Component Usage
-
Always Provide Complete Product Data
// Good: Complete product object product = signal<Product>({ ean: '9783161484100', name: 'The Great Gatsby', contributors: 'F. Scott Fitzgerald', format: 'HC', formatDetail: 'Hardcover', }); // Avoid: Partial product data may cause rendering issues product = signal<Product>({ ean: '123' }); // Missing name, contributors -
Use Immutable Data Patterns with OnPush
// Good: Create new reference this.product.set({ ...currentProduct, name: 'New Name' }); // Bad: Mutate existing object (won't trigger change detection) this.product().name = 'New Name'; -
Include E2E Attributes in Parent Components
<!-- Good: Add context-specific attributes --> <oms-shared-return-product-info [product]="product" data-what="component" data-which="return-product-info" [attr.data-product-ean]="product.ean" ></oms-shared-return-product-info> <!-- Minimal: Basic usage without additional attributes --> <oms-shared-return-product-info [product]="product" ></oms-shared-return-product-info> -
Handle Async Product Loading
// Good: Wait for product data before rendering @Component({ template: ` @if (product(); as prod) { <oms-shared-return-product-info [product]="prod" /> } @else { <div class="loading-skeleton"></div> } ` }) export class ProductContainerComponent { product = signal<Product | null>(null); }
Styling and Layout
-
Use Wrapper Divs for Additional Layout
<!-- Good: Wrap for additional styling without ::ng-deep --> <div class="custom-card p-4 border rounded"> <oms-shared-return-product-info [product]="product" /> </div> <!-- Avoid: Don't break encapsulation unless necessary --> <style> ::ng-deep .return-product-info { /* ... */ } </style> -
Respect Component's Fixed Image Width
<!-- Component uses w-14 (56px) - design other elements accordingly --> <div class="grid grid-cols-[56px_1fr_auto]"> <oms-shared-return-product-info [product]="product" /> <div>Additional content</div> </div>
Performance
-
Use trackBy in Lists
<!-- Good: Track by unique identifier --> @for (product of products(); track product.ean) { <oms-shared-return-product-info [product]="product" /> } <!-- Avoid: No tracking causes unnecessary re-renders --> @for (product of products(); track $index) { <oms-shared-return-product-info [product]="product" /> } -
Leverage OnPush Change Detection
// Good: Component already uses OnPush, ensure parent does too @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ReturnProductInfoComponent] })
Accessibility
-
Product Images Already Have Alt Text
- Component binds
product.nameto imagealtattribute - No additional ARIA labels needed for images
- Component binds
-
Consider Screen Reader Context
<!-- Good: Add semantic context --> <article aria-label="Product Information"> <oms-shared-return-product-info [product]="product" /> </article>
Testing
-
Mock External Directives
import { MockDirective } from 'ng-mocks'; createComponentFactory({ component: ReturnProductInfoComponent, overrideComponents: [[ ReturnProductInfoComponent, { remove: { imports: [ProductImageDirective, ProductRouterLinkDirective] }, add: { imports: [ MockDirective(ProductImageDirective), MockDirective(ProductRouterLinkDirective) ]} } ]] }); -
Use E2E Attributes for Selectors
// Good: Stable selectors independent of styling const name = spectator.query('[data-what="product-name"]'); // Avoid: Class-based selectors are fragile const name = spectator.query('.return-product-info__name'); -
Test with Realistic Product Data
// Good: Complete mock product const MOCK_PRODUCT: Product = { ean: '9783161484100', name: 'Complete Product Name', contributors: 'First Last, Second Author', format: 'HC', formatDetail: 'Hardcover', catalogProductNumber: 'CAT123', manufacturer: 'Publisher Name', };
Error Handling
-
Validate Product Data Before Passing to Component
// Good: Ensure product has required fields function isValidProduct(product: Product): boolean { return !!(product.ean && product.name && product.contributors); } @Component({ template: ` @if (isValidProduct(product())) { <oms-shared-return-product-info [product]="product()" /> } @else { <div class="error">Invalid product data</div> } ` }) -
Handle Missing Format Icons
// Component handles lowercase conversion // Ensure format codes match ProductFormatIconGroup: tb, hc, au, ka, sw, nb product.format = 'HC'; // Will become 'hc' and map to icon
License
Internal ISA Frontend library - not for external distribution.