mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
1. Reward Shopping Cart Implementation - New shopping cart with quantity control and availability checking - Responsive shopping cart item component with improved CSS styling - Shipping address integration in cart - Customer reward card and billing/shipping address components 2. Navigation State Management Library (@isa/core/navigation) - New library with type-safe navigation context service (373 lines) - Navigation state service (287 lines) for temporary state between routes - Comprehensive test coverage (668 + 227 lines of tests) - Documentation (792 lines in README.md) - Replaces query parameters for passing temporary navigation context 3. CRM Shipping Address Services - New ShippingAddressService with fetching and validation - CustomerShippingAddressResource and CustomerShippingAddressesResource - Zod schemas for data validation 4. Additional Improvements - Enhanced searchbox accessibility with ARIA support - Availability data access rework for better fetching/mapping - Storybook tooltip variant support - Vitest JUnit and Cobertura reporting configuration Related work items: #5382, #5383, #5384
669 lines
17 KiB
TypeScript
669 lines
17 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import { signal } from '@angular/core';
|
|
import { NavigationContextService } from './navigation-context.service';
|
|
import { TabService } from '@isa/core/tabs';
|
|
import { ReturnUrlContext } from './navigation-context.types';
|
|
import { NAVIGATION_CONTEXT_METADATA_KEY } from './navigation-context.constants';
|
|
|
|
describe('NavigationContextService', () => {
|
|
let service: NavigationContextService;
|
|
let tabServiceMock: {
|
|
activatedTabId: ReturnType<typeof signal<number | null>>;
|
|
entityMap: ReturnType<typeof vi.fn>;
|
|
patchTabMetadata: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Create mock TabService with signals and methods
|
|
tabServiceMock = {
|
|
activatedTabId: signal<number | null>(null),
|
|
entityMap: vi.fn(),
|
|
patchTabMetadata: vi.fn(),
|
|
};
|
|
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
NavigationContextService,
|
|
{ provide: TabService, useValue: tabServiceMock },
|
|
],
|
|
});
|
|
|
|
service = TestBed.inject(NavigationContextService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should be created', () => {
|
|
expect(service).toBeTruthy();
|
|
});
|
|
|
|
describe('setContext', () => {
|
|
it('should set context in tab metadata', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
const data: ReturnUrlContext = { returnUrl: '/test-page' };
|
|
|
|
// Act
|
|
await service.setContext(data);
|
|
|
|
// Assert
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(
|
|
tabId,
|
|
expect.objectContaining({
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: expect.objectContaining({
|
|
default: expect.objectContaining({
|
|
data,
|
|
createdAt: expect.any(Number),
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should set context with custom scope', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const customScope = 'customer-details';
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
const data = { customerId: 42 };
|
|
|
|
// Act
|
|
await service.setContext(data, customScope);
|
|
|
|
// Assert
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(
|
|
tabId,
|
|
expect.objectContaining({
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: expect.objectContaining({
|
|
[customScope]: expect.objectContaining({
|
|
data,
|
|
createdAt: expect.any(Number),
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should throw error when no active tab', async () => {
|
|
// Arrange
|
|
tabServiceMock.activatedTabId.set(null);
|
|
|
|
// Act & Assert
|
|
await expect(service.setContext({ returnUrl: '/test' })).rejects.toThrow(
|
|
'No active tab - cannot set navigation context',
|
|
);
|
|
});
|
|
|
|
it('should merge with existing contexts', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
|
|
const existingContexts = {
|
|
'existing-scope': {
|
|
data: { existingData: 'value' },
|
|
createdAt: Date.now() - 1000,
|
|
},
|
|
};
|
|
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: existingContexts,
|
|
},
|
|
},
|
|
});
|
|
|
|
const newData = { returnUrl: '/new-page' };
|
|
|
|
// Act
|
|
await service.setContext(newData);
|
|
|
|
// Assert
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(
|
|
tabId,
|
|
expect.objectContaining({
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: expect.objectContaining({
|
|
'existing-scope': existingContexts['existing-scope'],
|
|
default: expect.objectContaining({
|
|
data: newData,
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should accept TTL parameter for backward compatibility', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
await service.setContext({ returnUrl: '/test' }, undefined, 60000);
|
|
|
|
// Assert - TTL is ignored but method should still work
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('getContext', () => {
|
|
it('should return null when no active tab', async () => {
|
|
// Arrange
|
|
tabServiceMock.activatedTabId.set(null);
|
|
|
|
// Act
|
|
const result = await service.getContext();
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should return null when context does not exist', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getContext();
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should retrieve context from default scope', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const data: ReturnUrlContext = { returnUrl: '/test-page' };
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
default: {
|
|
data,
|
|
createdAt: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getContext<ReturnUrlContext>();
|
|
|
|
// Assert
|
|
expect(result).toEqual(data);
|
|
});
|
|
|
|
it('should retrieve context from custom scope', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const customScope = 'checkout-flow';
|
|
const data = { step: 2, productId: 456 };
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
[customScope]: {
|
|
data,
|
|
createdAt: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getContext(customScope);
|
|
|
|
// Assert
|
|
expect(result).toEqual(data);
|
|
});
|
|
|
|
it('should return null when tab not found', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({});
|
|
|
|
// Act
|
|
const result = await service.getContext();
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should handle invalid metadata gracefully', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: 'invalid', // Invalid type
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getContext();
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getAndClearContext', () => {
|
|
it('should return null when no active tab', async () => {
|
|
// Arrange
|
|
tabServiceMock.activatedTabId.set(null);
|
|
|
|
// Act
|
|
const result = await service.getAndClearContext();
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should retrieve and remove context from default scope', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const data: ReturnUrlContext = { returnUrl: '/test-page' };
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
default: {
|
|
data,
|
|
createdAt: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getAndClearContext<ReturnUrlContext>();
|
|
|
|
// Assert
|
|
expect(result).toEqual(data);
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {},
|
|
});
|
|
});
|
|
|
|
it('should retrieve and remove context from custom scope', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const customScope = 'wizard-flow';
|
|
const data = { currentStep: 3 };
|
|
const otherScopeData = { otherData: 'value' };
|
|
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
[customScope]: {
|
|
data,
|
|
createdAt: Date.now(),
|
|
},
|
|
'other-scope': {
|
|
data: otherScopeData,
|
|
createdAt: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getAndClearContext(customScope);
|
|
|
|
// Assert
|
|
expect(result).toEqual(data);
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
'other-scope': expect.objectContaining({
|
|
data: otherScopeData,
|
|
}),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should return null when context not found', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.getAndClearContext();
|
|
|
|
// Assert
|
|
expect(result).toBeNull();
|
|
expect(tabServiceMock.patchTabMetadata).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('clearContext', () => {
|
|
it('should return true when context exists and is cleared', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const data = { returnUrl: '/test' };
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
default: { data, createdAt: Date.now() },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.clearContext();
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when context not found', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.clearContext();
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('clearScope', () => {
|
|
it('should clear all contexts for active tab', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const contexts = {
|
|
default: { data: { url: '/test' }, createdAt: Date.now() },
|
|
'scope-1': { data: { value: 1 }, createdAt: Date.now() },
|
|
'scope-2': { data: { value: 2 }, createdAt: Date.now() },
|
|
};
|
|
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: contexts,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const clearedCount = await service.clearScope();
|
|
|
|
// Assert
|
|
expect(clearedCount).toBe(3);
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {},
|
|
});
|
|
});
|
|
|
|
it('should return 0 when no contexts exist', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const clearedCount = await service.clearScope();
|
|
|
|
// Assert
|
|
expect(clearedCount).toBe(0);
|
|
expect(tabServiceMock.patchTabMetadata).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return 0 when no active tab', async () => {
|
|
// Arrange
|
|
tabServiceMock.activatedTabId.set(null);
|
|
|
|
// Act
|
|
const clearedCount = await service.clearScope();
|
|
|
|
// Assert
|
|
expect(clearedCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('clearAll', () => {
|
|
it('should clear all contexts for active tab', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const contexts = {
|
|
default: { data: { url: '/test' }, createdAt: Date.now() },
|
|
};
|
|
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: contexts,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
await service.clearAll();
|
|
|
|
// Assert
|
|
expect(tabServiceMock.patchTabMetadata).toHaveBeenCalledWith(tabId, {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('hasContext', () => {
|
|
it('should return true when context exists', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const data = { returnUrl: '/test' };
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
default: { data, createdAt: Date.now() },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.hasContext();
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when context does not exist', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.hasContext();
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should check custom scope', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const customScope = 'wizard';
|
|
const data = { step: 1 };
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: {
|
|
[customScope]: { data, createdAt: Date.now() },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const result = await service.hasContext(customScope);
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getContextCount', () => {
|
|
it('should return total number of contexts for active tab', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
const contexts = {
|
|
default: { data: { url: '/test' }, createdAt: Date.now() },
|
|
'scope-1': { data: { value: 1 }, createdAt: Date.now() },
|
|
'scope-2': { data: { value: 2 }, createdAt: Date.now() },
|
|
};
|
|
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {
|
|
[NAVIGATION_CONTEXT_METADATA_KEY]: contexts,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const count = await service.getContextCount();
|
|
|
|
// Assert
|
|
expect(count).toBe(3);
|
|
});
|
|
|
|
it('should return 0 when no contexts exist', async () => {
|
|
// Arrange
|
|
const tabId = 123;
|
|
tabServiceMock.activatedTabId.set(tabId);
|
|
tabServiceMock.entityMap.mockReturnValue({
|
|
[tabId]: {
|
|
id: tabId,
|
|
name: 'Test Tab',
|
|
metadata: {},
|
|
},
|
|
});
|
|
|
|
// Act
|
|
const count = await service.getContextCount();
|
|
|
|
// Assert
|
|
expect(count).toBe(0);
|
|
});
|
|
|
|
it('should return 0 when no active tab', async () => {
|
|
// Arrange
|
|
tabServiceMock.activatedTabId.set(null);
|
|
|
|
// Act
|
|
const count = await service.getContextCount();
|
|
|
|
// Assert
|
|
expect(count).toBe(0);
|
|
});
|
|
});
|
|
});
|